LinqPAD Script of the Day: RandomStringGenerator





0/5 (0 vote)
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).
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!