10
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ Epic title! That's pretty awesome. \$\endgroup\$ – Phrancis Apr 28 '15 at 19:14
  • \$\begingroup\$ Nice title, but what question do you have ? ;-) \$\endgroup\$ – Heslacher Apr 28 '15 at 19:34
  • 2
    \$\begingroup\$ I guess my question would be: how badly have I coded this while sleep-deprived? \$\endgroup\$ – Nick Udell Apr 28 '15 at 20:35
5
\$\begingroup\$

At a quick glance the ControlStringFinder.FindAllControlStrings() method would benefit from the usage of the String.IndexOf(char, int) method.

public IEnumerable<ControlString> FindAllControlStrings(string input)
{
    for (var i = 0; i < input.Length; i++)
    {
        if (input[i] == controlStringStarter)
        {
            var end = input.IndexOf(controlStringTerminator, i + 1);

            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;
        }
    }
}

To be on the safe side for changing accidentially the values of the controlStringStarter, controlStringTerminator or valueSeparator in the ControlStringFinder class you should make them readonly.


None of the properties of the classes needs to be public writeable. For encapsulation you should make the setters private.


Instead of throwing an ArgumentException with the same message 3 times I would suggest to throw a MatchException with an overloaded constructor which sets the message to this repeated value.


In its current state a consumer of the Parser class can't know what the values for controlStringStarter, controlStringTerminator or valueSeparator should be. Also, if you want to use other chars for this purpose you would need to change the Parser class.

This would be a good place to use properties with public getters and setters.


public Person(Pronoun pronouns, Region region)  

This looks strange. A parameter of a Pronoun type which is called pronouns which is indicating that it is more than one Pronoun. Better use the singular.

\$\endgroup\$
  • \$\begingroup\$ Spot on! Clearly I need more sleep before I code. In the case of pronouns I think perhaps I should rename the class to Pronouns because it contains a set of pronouns (e.g. he, him, his, etc) for different situations. \$\endgroup\$ – Nick Udell Apr 29 '15 at 11:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.