I'm building a random text generator!
My idea was to borrow slightly from the string.Format
method styling, have a "format string" that controlled the overall construction of the string, and include "control string" segments inside them that classes will inject text into depending on their own logic.
For example, the input string:
Before you stands {Person:FullName}.
Where {Person:FullName}
expands to:
{Prefix} {Name} {Postfix}, {Rank} of {Region:FullName}
Generates:
Before you stands The Barbarous Pompey The Child, Maharajah of The The Ephemeral Citystate of Charr.
Before you stands Bertram The Churlish, Prefect of County of Dragons.
Before you stands The Honourable Launce The Child, Pastor of The Stormy Duchy of Key.
Before you stands The Roguish Parolles The Holy, Grand Duke of Sands of Avarice.
Before you stands The Barbarous Dogberry The Great, Colonel of Elvenhome.
Before you stands The Cowardly Lysander The Sinner, Archon of Principality of Elvenhome.
Before you stands The Honourable Snug The Holy, Saopha of The Shining Principality of Old Thatch.
The code
public class Parser
{
private IControlStringMatcher matcher;
public Parser(IControlStringMatcher matcher)
{
this.matcher = matcher;
}
public string Parse(string input)
{
var finder = new ControlStringFinder('{', ':', '}');
var controlStrings = finder.FindAllControlStrings(input);
string result = input;
foreach (var controlString in controlStrings)
{
if (matcher.Matches(controlString))
{
var originalString = input.Substring(controlString.Index, controlString.Length);
var newString = matcher.Match(controlString);
result = result.Replace(originalString, newString);
}
}
return result;
}
}
Parser
is designed to split up the input string and stitch the replacement parts back together. It doesn't decide what goes where, that's up to the IControlStringMatcher
instance.
public class ControlStringFinder
{
private char controlStringStarter;
private char controlStringTerminator;
private char valueSeparator;
public ControlStringFinder(char controlStringStarter, char valueSeparator, char controlStringTerminator)
{
this.controlStringStarter = controlStringStarter;
this.valueSeparator = valueSeparator;
this.controlStringTerminator = controlStringTerminator;
}
public IEnumerable<ControlString> FindAllControlStrings(string input)
{
for (var i = 0; i < input.Length; i++)
{
if (input[i] == controlStringStarter)
{
var end = -1;
for (var j = i + 1; j < input.Length; j++)
{
if (input[j] == controlStringTerminator)
{
end = j;
break;
}
}
if (end == -1)
{
throw new FormatException("Input string has opening control string starter with no matching control string terminator.");
}
var internalString = input.Substring(i + 1, end - i - 1);
var values = new Queue<string>(internalString.Split(valueSeparator));
yield return new ControlString(i, end - i + 1, values);
i = end;
}
}
}
}
ControlStringFinder
is a helper class that simply extracts all control strings from an input string.
public class ControlString
{
public int Index { get; set; }
public Queue<string> Values { get; set; }
public ControlString(int index, int length, Queue<string> values)
{
this.Index = index;
this.Values = values;
this.Length = length;
}
public ControlString NextControlString
{
get
{
return new ControlString(Index + Values.Peek().Length, Length - Values.Peek().Length, new Queue<string>(Values.Skip(1)));
}
}
public int Length { get; set; }
}
A control string is a simple class containing data pertaining to the control strings found in the input string. Index refers to its position in the original string, to help the Parser
work out where to make the replacement.
public interface IControlStringMatcher
{
bool Matches(ControlString controlString);
string Match(ControlString controlString);
}
Pretty simply interface for IControlStringMatcher
, whether this control string can be replaced by the implementer, and the actual replacement itself.
public class ControlStringMatcherCollection : IControlStringMatcher
{
public IEnumerable<IControlStringMatcher> ControlStringMatchers { get; set; }
public ControlStringMatcherCollection(IEnumerable<IControlStringMatcher> controlStringMatchers)
{
this.ControlStringMatchers = controlStringMatchers;
}
public bool Matches(ControlString controlString)
{
return ControlStringMatchers.Any(x => x.Matches(controlString));
}
public string Match(ControlString controlString)
{
if (!Matches(controlString))
{
throw new ArgumentException("Argument cannot be matched by this matcher.", "controlString");
}
return ControlStringMatchers.First(x => x.Matches(controlString)).Match(controlString);
}
}
.
public class ContextControlStringMatcher : IControlStringMatcher
{
public string Context { get; set; }
public IControlStringMatcher Matcher { get; private set; }
public ContextControlStringMatcher(string context, IControlStringMatcher matcher)
{
if(matcher == null)
{
throw new ArgumentNullException("matcher");
}
this.Context = context;
this.Matcher = matcher;
}
public bool Matches(ControlString controlString)
{
try
{
return controlString.Values.Peek().Equals(Context) && Matcher.Matches(controlString.NextControlString);
}
catch(Exception)
{
Debug.Log("Error processing controlString " + string.Join(":", controlString.Values.ToArray()));
throw;
}
}
public string Match(ControlString controlString)
{
if (!Matches(controlString))
{
throw new ArgumentException("Argument cannot be matched by this matcher.", "controlString");
}
return Matcher.Match(controlString.NextControlString);
}
}
.
public class FuncControlStringMatcher : IControlStringMatcher
{
public Func<ControlString, bool> CanMatch { get; set; }
public Func<ControlString, string> Matcher { get; set; }
public FuncControlStringMatcher(Func<ControlString, string> matcher, Func<ControlString, bool> canMatch)
{
this.Matcher = matcher;
this.CanMatch = canMatch;
}
public bool Matches(ControlString controlString)
{
return CanMatch(controlString);
}
public string Match(ControlString controlString)
{
if (!Matches(controlString))
{
throw new ArgumentException("Argument cannot be matched by this matcher.", "controlString");
}
return Matcher(controlString);
}
}
These three are the most common of the IControlStringMatcher
instances.
The first, fairly simply, represents a collection of them and runs Match
on the first instance that Matches
returns a true for.
The second allows the navigation of control string context. For example, the control string {Person:Name}
can be matched by a ContextControlStringMatcher
instance with Context "Person". Doing so strips the string Person
from the control string, and passes {Name} to the ContextControlStringMatcher
instance's internal IControlStringMatcher
.
The last allows you to pass Func
instances for both Match
and Matches
. This is typically how I set up end-point values, instead of coding them into the methods themselves.
public class Person : IControlStringMatcher
{
public string prefix;
public string name;
public string rank;
public Region Region { get; set; }
public string postfix;
public string FullName
{
get
{
return new Parser(Matchers).Parse("{Prefix} {Name} {Postfix}, {Rank} of {Region:FullName}");
}
}
public Pronoun Pronouns { get; set; }
public ControlStringMatcherCollection Matchers { get; set; }
public Person(Pronoun pronouns, Region region)
{
this.Pronouns = pronouns;
this.Region = region;
Matchers = new ControlStringMatcherCollection(new List<IControlStringMatcher>()
{
new ContextControlStringMatcher("Pronoun", Pronouns),
new ValueControlStringMatcher("Prefix", ()=> prefix),
new ValueControlStringMatcher("Postfix", ()=> postfix),
new ContextControlStringMatcher("Region", Region),
new ValueControlStringMatcher("Rank", ()=> rank),
new ValueControlStringMatcher("Name", ()=> name),
new ValueControlStringMatcher("FullName", ()=> FullName)
});
}
public bool Matches(ControlString controlString)
{
return Matchers.Matches(controlString);
}
public string Match(ControlString controlString)
{
if (!Matches(controlString))
{
throw new ArgumentException("Argument cannot be matched by this matcher.", "controlString");
}
return Matchers.Match(controlString);
}
}
Here, ValueControlStringMatcher
is a simple combination of a ContextControlStringMatcher
and a FuncControlStringMatcher
for ease of use.