Your code is easy to understand and very performant.
Some improvements:
- String for start / end tag could be stored as constant. That has the advantage that it can be changed on one central location and the length of the string can be accessed like
i += START_TAG.Length - When using a
StringBuilder instead of of the char array newText, the running variable 'j' can be dropped. - The 2 code parts
if (text.IndexOf("<upcase>", k) < 0) // check if there is any <upcase> tag { // if no, copy everyting while (i < text.Length) { textNew[j++] = text[i++]; } } else { while (i < text.IndexOf("<upcase>", k)) // if there is an <upcase> tag, copy letters until the tag { textNew[j++] = text[i++]; } i += 8; // move index i to the position right next to the <upcase> tag k = i; }
and
if (text.IndexOf("</upcase>", k) < 0) // check if there is any </upcase> tag { // if no, copy everyting in CAPITAL letters while (i < text.Length) { textNew[j++] = Char.ToUpper(text[i++]); } } else { while (i < text.IndexOf("</upcase>", k)) // if there is an </upcase> tag, copy letters in CAP letters until the tag { textNew[j++] = Char.ToUpper(text[i++]); } i += 9; // move index i to the position right next to the </upcase> tag }
are very simlar. Probably it is possible to create one more generic code fragment that coveres both cases.
Since your solution is still understandable for such a simple use case, it will fast become unmaintainable if the use case becomes more complex. Therefore, it makes sense to think about a more abstract OOP concepts to model the solution.
One alternative impl. (that is probably over engineered for the given problem) gives an idea how a more object oriented design could look like:
public class Tag { private readonly Func<char, char> map; public Tag(string start, string end, Func<char, char> map) { this.Start = start; this.End = end; this.map = map; } public string Start { get; } public string End { get; } public char Map(char input) => this.map(input); } public class TagProcessor { private readonly Tag tag; private readonly StringBuilder output = new StringBuilder(); private string input; private bool isTagOpen; private int index; public TagProcessor(Tag tag) { this.tag = tag; } public string Process(string input) { this.input = input; this.index = 0; this.isTagOpen = false; this.output.Clear(); do { var tagProcessed = this.TryOpenTag() || this.TryCloseTag(); if (!tagProcessed) { this.ApplyCurrentChar(); } } while (this.MoveNext()); return output.ToString(); } private bool IsEndTag() => input.IndexOf(tag.End, this.index) == this.index; private bool IsStartTag() => input.IndexOf(tag.Start, this.index) == this.index; private bool MoveNext() { index++; return index < this.input.Length; } private void ApplyCurrentChar() { var inputChar = this.input[this.index]; var transfomed = this.isTagOpen ? tag.Map(inputChar) : inputChar; this.output.Append(transfomed); } private bool TryOpenTag() { if (!isTagOpen && IsStartTag()) { this.index += this.tag.Start.Length - 1; this.isTagOpen = true; return true; } return false; } private bool TryCloseTag() { if (isTagOpen && IsEndTag()) { this.index += this.tag.End.Length - 1; this.isTagOpen = false; return true; } return false; } } public static void Main(string[] args) { var processor = new TagProcessor(new Tag("<upcase>", "</upcase>", char.ToUpper)); var test = new[] { "abc<upcase>test</upcase>", "abc<upcase>test", "abc<upcase></upcase>test", "abc<upcase>test</upcase>test", "abc<upcase>te<upcase>st</upcase>test", "a</upcase>bc<upcase>te<upcase>st</upcase>te</upcase>st", }; foreach (var t in test) Console.WriteLine(t + ": " + processor.Process(t)); Console.ReadLine(); }
The advantages are, that this solution remains readable if the complexity grows (e.g. more tags were added) and it allows to change / extend the logic without understanding the whole parsing logic. Further more, each method has a single pupose which increases comprehensibleness.