Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Visual-Studio

LinqPAD Script of the Day: RandomStringGenerator

0.00/5 (No votes)
13 Nov 2018CPOL3 min read 1.6K  
RandomStringGenerator

Introduction

This is part of a series entitled LinqPAD script of the day in which we will be looking at some scripts I used from time to time to help me with a specific issue or question.

Keep in mind that some of these scripts might be silly, useless, geeky but hey, whatever helps right? 😀

All the scripts will have a GitHub link provided so you can inspect, download and play with them. Also please note that some scripts might require nuget packages, so for those scripts, you will need either a developer license or higher; as an alternative, create a Visual Studio project and compile it in Visual Studio (or Visual Studio code if it works).
If you wish, you might even integrate the logic of a script into your own application if it satisfies one of your needs.

Now to the matter at hand; I have written a showcase about a tool called LinqPad which has been my de facto tool for running small snippets of code or just as a scratch pad.

The last script of the day was WorldClock.

Today, we will discuss a script I called RandomStringGenerator (link can be found here).

What Does It Do?

Well, it generates random strings of course :D. Jokes aside, whenever I needed a random string either in my tests or applications, I would default to calling Guid.NewGuid().ToString(), and yes, most of the time, this will work except for the following scenarios:

  • You want to enforce the length of the string, maybe from a database or business rule point of view
  • You want a bit more variety and control over what characters are used and how many

So I wrote this little monstrosity (it was way shorter and specific but for the purposes of this post, it has been redesigned a bit to cover a more general approach).

C#
void Main()
{
    // setting up the rules we want to be applied to our random string generator
    StringGenerationRule[] rules =
    {
        new DigitStringGenerationRule(2),
        new LowerCaseStringGenerationRule(1),
        new UpperCaseStringGenerationRule(1),
        new SymbolStringGenerationRule(1),

        // because each rule needs to be applied the number of time 
        // specified this will make it run a bit slower but it showcases what the 
        // custom string strategy can enforce
        new CustomStringGenerationRule("¶", 1),

        // added a second custom rule to validate that the check for duplicates is done 
        // via pattern instead of type
        new CustomStringGenerationRule("↨", 1)
    };

    new RandomStringGenerator(rules).Generate().Dump(); // generating the string
}

/// <summary>
/// Generates a random string based on the rules provided tot it
/// </summary>
class RandomStringGenerator
{
    private readonly IEnumerable<StringGenerationRule> _stringGenerationRules;

    /// <summary>
    /// Creates an instance of the generator
    /// </summary>
    /// <param name="rules">The rules that the generator needs to follow when creating a string
    /// </param>
    public RandomStringGenerator(params StringGenerationRule[] rules)
    {
        var groupedRules = rules.GroupBy(a => a.StringPattern); // we group all the rules 
                                                        // so that we can verify their validity

        // we check if there are any duplicate rules
        if (groupedRules.Any(a => a.Count() > 1))
        {
            var duplicatePatterns = groupedRules.Where(grouping => grouping.Count() > 1).Select
                            (grouping => grouping.Key); // extract the duplicate rules

            // throw an exception letting us know that we have 
            // duplicate rules and what are those rules.
            throw new InvalidOperationException($"The rules need to be distinct, 
                     found duplicate rules at [{string.Join(",", duplicatePatterns)}]");
        }

        _stringGenerationRules = rules;
    }

    /// <summary>
    /// Generates the string
    /// </summary>
    /// <returns>A string that follows the length and rules</returns>
    public string Generate()
    {
        string str = string.Empty; // this will be the output string

        Random random = new Random();
        int limit = _stringGenerationRules.Sum(s => s.NumberOfCharacters); // here we calculate 
                        // the length of the string that needs to be generated based on how many 
                        // rules needs to be applied

        // while the length has not reached the limit, keep on appending to the string
        while (str.Length < limit)
        {
            char character; // the character that will be appended to the string
            do
            {
                character = (char)random.Next(char.MaxValue); // get a random character 
                                    // out of the whole set of characters in the system
            }
            while (!_stringGenerationRules.Any(s => s.CanExecute(character, str))); // while 
                // the character doesn't fulfill any of the rules, keep choosing characters at random 

            StringGenerationRule validRule =
                _stringGenerationRules.First(s => s.CanExecute(character, str)); // get the 
                                                                         // first rule that applies

            str = validRule.Execute(character, str); // apply the rule and update the string
        }

        return str;
    }
}

/// <summary>
/// This is the base class for all the rules
/// </summary>
abstract class StringGenerationRule
{
    private readonly Regex Pattern;

    /// <summary>
    /// Represents the number of characters a rule has to apply
    /// </summary>
    public readonly int NumberOfCharacters;

    /// <summary>
    /// Sets up the requirements for when a concrete class is instantiated
    /// </summary>
    /// <param name="numberOfCharacters">The number of characters that will be 
    ///  applied by the rule</param>
    /// <param name="pattern">The pattern that the rule needs to follow</param>
    public StringGenerationRule(int numberOfCharacters, string pattern)
    {
        NumberOfCharacters = numberOfCharacters;
        Pattern = new Regex(pattern, RegexOptions.Compiled); // Here we set the pattern 
                                            // to Compiled so that is a bit more efficient
    }

    /// <summary>
    /// The pattern of the rule
    /// </summary>
    public string StringPattern => Pattern.ToString();

    /// <summary>
    /// Verifies if the rule can be applied to the current string
    /// </summary>
    /// <param name="character">The character that will be checked</param>
    /// <param name="currentString">The generated string so far</param>
    /// <returns>True if the character is valid and can be added to the string</returns>
    public bool CanExecute(char character, string currentString)
    {
        string stringOfchar = character.ToString(); // we cast the character to a string 
                                                    // so that we can verify it

        // if the character string is null or empty 
        // then the rule cannot be applied so we return false
        if (string.IsNullOrEmpty(stringOfchar))
        {
            return false;
        }

        bool isValidCharacter = Pattern.IsMatch(stringOfchar); // we validate 
                                                               // the char based on our rule
        bool isRoomForMoreCharacters = 
               Pattern.Matches(currentString).Count < NumberOfCharacters; // we check if we 
                              // reached the intended number of characters in that string
        return isValidCharacter && isRoomForMoreCharacters; // if it's valid and we can add 
                                                            // characters then we will return true
    }

    /// <summary>
    /// Executes the rule by concatenating the validated character to the current string
    /// </summary>
    /// <param name="character">The character to append</param>
    /// <param name="currentString">The current string before execution</param>
    /// <returns>The new string with the appended character</returns>
    public string Execute(char character, string currentString)
    {
        // Added this check in case someone forgets to check if a rule can be applied.
        if (!CanExecute(character, currentString))
        {
            return currentString;
        }

        return string.Concat(currentString, character);
    }
}

/// <summary>
/// Represents a rule for digits
/// </summary>
class DigitStringGenerationRule : StringGenerationRule
{
    public DigitStringGenerationRule(int numberOfCharacters)
        : base(numberOfCharacters, @"[0-9]")
    {
    }
}

/// <summary>
/// Represents a rule for lower case characters
/// </summary>
class LowerCaseStringGenerationRule : StringGenerationRule
{
    public LowerCaseStringGenerationRule(int numberOfCharacters)
        : base(numberOfCharacters, @"[a-z]")
    {
    }
}

/// <summary>
/// Represents a rule for upper case characters
/// </summary>
class UpperCaseStringGenerationRule : StringGenerationRule
{
    public UpperCaseStringGenerationRule(int numberOfCharacters)
        : base(numberOfCharacters, @"[A-Z]")
    {
    }
}

/// <summary>
/// Represents a rule for commonly used symbols
/// </summary>
class SymbolStringGenerationRule : StringGenerationRule
{
    public SymbolStringGenerationRule(int numberOfCharacters)
        : base(numberOfCharacters, @"[!@$%&*-]")
    {
    }
}

/// <summary>
/// Represents a rule that can be given a custom pattern
/// </summary>
class CustomStringGenerationRule : StringGenerationRule
{
    public CustomStringGenerationRule(string pattern, int numberOfCharacters)
        : base(numberOfCharacters, pattern)
    {
    }
}

I tried to document as much as I can wherever the need arises, but as you can see in the Main method, all we need to do is give the generator a collection of rules (notice the params keyword in the constructor allowing us to pass in the rules by hand instead of wrapping them in a collection first).

So this generator leverages the power of Regex, but since it’s mostly char centric instead of string, our rules need to be small in the sense that they should only check for one potential character.

Where Can I Use It?

As mentioned before, these are some of the scenarios in which this would help:

  • Generating random passwords
  • Testing out the password validation on an application, just apply your business rules
  • Exploratory testing (nudge nudge topic for a new post 🙂 )

Again, this might be overkill for a script but it was fun making it and employing different design approaches before it reached this form. And I think it’s somewhat good enough to be used in applications as is, validation exceptions and all.

Thank you and I hope you enjoyed today’s LinqPAD script of the day.

Cheers!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)