Last active
June 20, 2024 23:21
-
-
Save deanebarker/befe90bc93a8d433cb4d7cc050592c95 to your computer and use it in GitHub Desktop.
A Dictionary<string, long> that can be maniuplated by a simple, user-friendly language
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Parlot.Fluent; | |
using static Parlot.Fluent.Parsers; | |
using System.Collections.Generic; | |
using System; | |
using System.Linq; | |
/* | |
This class allows you to query and manipulate a set of numbers using a simple language. | |
For a practical (?) usage, see: https://www.linkedin.com/posts/deane_last-week-i-made-a-post-about-how-im-contemplating-activity-7208915589346574336-wra | |
For full doc, see https://deanebarker.net/tech/code/cvc/ | |
Available commands: | |
show | |
set [NAME] to [NUMBER] | |
remove [NAME] | |
what is [NAME] | |
increase [NAME] by [NUMBER] | |
decrease [NAME] by [NUMBER] | |
increment [NAME] | |
decrement [NAME] | |
does [NAME] exist | |
[NAME] exists | |
is [NAME] equal to [NUMBER] | |
[NAME] is equal to [NUMBER] | |
is [NAME] more than [NUMBER] | |
[NAME] is more than [NUMBER] | |
is [NAME] less than [NUMBER] | |
[NAME] is less than [NUMBER] | |
is [NAME] between [NUMBER] and [NUMBER] | |
[NAME] is between [NUMBER] and [NUMBER] | |
Basic usage: | |
var values = new ConversationalVariableCollection() { | |
["AGE"] = 52, | |
}; | |
values.Command("increase AGE by 1"); // AGE is now 53 | |
values.Command("increment AGE"); // AGE is now 54 | |
values.Command("is AGE equal to 50"); // False | |
values.Command("is AGE between 1 and 100"); // True | |
values.Command("is AGE less than 50"); // False | |
values.Command("does AGE exist"); // True | |
values.Command("what is AGE"); // 54 | |
values.Command("set IQ to 90"); // Now it has two variables | |
values.Command("remove IQ"); // Back to one... | |
*/ | |
namespace DeaneBarker.Utils | |
{ | |
public class ConversationalVariableCollection : Dictionary<string, long> | |
{ | |
// The reponse to return from Command when everything is good | |
// Defaults to NULL, but maybe you want it to say "OK" or something... | |
public string OkResponse { get; set; } = null; | |
private readonly Parser<VariableOperation> executeCommandParser; | |
private readonly Parser<VariableComparison> checkValueParser; | |
private readonly Parser<string> retrieveValueParser; | |
private readonly Parser<VariableExistence> checkExistenceQuestionParser; | |
private readonly Parser<VariableExistence> checkExistenceDeclarationParser; | |
private readonly Parser<string> removeParser; | |
private readonly Parser<VariableOperation> adjustParser; | |
public ConversationalVariableCollection() | |
{ | |
executeCommandParser = | |
OneOf(Terms.Text("increase"), Terms.Text("decrease"), Terms.Text("set")) | |
.And(Terms.NonWhiteSpace()) | |
.AndSkip(ZeroOrOne(OneOf(Terms.Text("by"), Terms.Text("to")))) | |
.And(Terms.Integer()).Then(x => new VariableOperation() | |
{ | |
Name = x.Item2.ToString(), | |
Operation = x.Item1, | |
Value = x.Item3 | |
}); | |
retrieveValueParser = | |
Terms.Text("what") | |
.SkipAnd(Terms.Text("is")) | |
.SkipAnd(Terms.NonWhiteSpace()) | |
.Then(x => x.ToString()); | |
checkValueParser = | |
ZeroOrOne(Terms.Text("is")) | |
.SkipAnd(Terms.NonWhiteSpace()) // variable name | |
.AndSkip(ZeroOrOne(Terms.Text("is"))) | |
.And(OneOf( | |
Terms.Text("equal").And(Terms.Text("to")).Then(x => "=="), | |
Terms.Text("more").And(Terms.Text("than")).Then(x => ">"), | |
Terms.Text("less").And(Terms.Text("than")).Then(x => "<"), | |
Terms.Text("between").Then(x => "><"), | |
Terms.Text("at").And(Terms.Text("least")).Then(x => ">="), | |
OneOf(Terms.Text("not"), Terms.Text("no")).And(Terms.Text("more")).And(Terms.Text("than")).Then(x => "<="), | |
Terms.Text("not").And(Terms.Text("equal").And(Terms.Text("to"))).Then(x => "!=") | |
)) | |
.And(OneOf( | |
Terms.Integer().AndSkip(Terms.Text("and")).And(Terms.Integer()).Then(v => new Bounding(v.Item2, v.Item1)), | |
Terms.Integer().Then(v => new Bounding(v, 0)) | |
)) | |
.Then(x => new VariableComparison() | |
{ | |
Name = x.Item1.ToString(), | |
Operator = x.Item2, | |
Value = x.Item3 | |
}); | |
checkExistenceQuestionParser = | |
Terms.Text("does") | |
.SkipAnd(Terms.NonWhiteSpace()) | |
.AndSkip(Terms.Text("exist")) | |
.Then(v => new VariableExistence() | |
{ | |
Name = v.ToString(), | |
ShouldExist = true | |
}); | |
checkExistenceDeclarationParser = | |
OneOf( | |
Terms.NonWhiteSpace().AndSkip(Terms.Text("does")).And(Terms.Text("not")).AndSkip(Terms.Text("exist")), | |
Terms.NonWhiteSpace().And(Terms.Text("exists")) | |
).Then(v => new VariableExistence() | |
{ | |
Name = v.Item1.ToString(), | |
ShouldExist = v.Item2 == "exists" | |
}); | |
removeParser = | |
Terms.Text("remove") | |
.SkipAnd(Terms.NonWhiteSpace()) | |
.Then(x => x.ToString()); | |
adjustParser = | |
OneOf(Terms.Text("increment"), Terms.Text("decrement")) | |
.And(Terms.NonWhiteSpace()) | |
.Then(x => new VariableOperation() | |
{ | |
Name = x.Item2.ToString(), | |
Operation = (x.Item1 == "increment") ? "increase" : "decrease", | |
Value = 1 | |
}); | |
} | |
private void InitVariable(string name) | |
{ | |
name = NormalizeVariableName(name); | |
if (!ContainsKey(name)) | |
{ | |
this[name] = 0; | |
} | |
} | |
private void SetValue(string name, long value) | |
{ | |
// Centralized this just to make sure the variable name gets consistently normalized | |
InitVariable(name); | |
this[NormalizeVariableName(name)] = value; | |
} | |
// This is for commands | |
public void Do(string commandString) | |
{ | |
commandString = NormalizeCommand(commandString); | |
var command = executeCommandParser.Parse(commandString); | |
if (command.Operation == "increase") | |
SetValue(command.Name, this[command.Name] + command.Value); | |
if (command.Operation == "decrease") | |
SetValue(command.Name, this[command.Name] - command.Value); | |
if (command.Operation == "set") | |
SetValue(command.Name, command.Value); | |
} | |
// This is for questions (anything that should return a bool) | |
public bool Ask(string questionString) | |
{ | |
questionString = NormalizeCommand(questionString); | |
var existence = checkExistenceQuestionParser.Parse(questionString); | |
if (existence != null) | |
{ | |
return ContainsKey(existence.Name); | |
} | |
existence = checkExistenceDeclarationParser.Parse(questionString); | |
if (existence != null) | |
{ | |
return ContainsKey(existence.Name) == existence.ShouldExist; | |
} | |
var question = checkValueParser.Parse(questionString); | |
if (!ContainsKey(question.Name)) | |
return false; // Variable doesn't exist | |
if (question.Operator == ">") | |
return this[question.Name] > question.Value.Upper; | |
if (question.Operator == "<") | |
return this[question.Name] < question.Value.Upper; | |
if (question.Operator == "==") | |
return this[question.Name] == question.Value.Upper; | |
if (question.Operator == "><") | |
return this[question.Name] >= question.Value.Lower && this[question.Name] <= question.Value.Upper; | |
if (question.Operator == "!=") | |
return this[question.Name] != question.Value.Upper; | |
if (question.Operator == ">=") | |
return this[question.Name] >= question.Value.Upper; | |
if (question.Operator == "<=") | |
return this[question.Name] <= question.Value.Upper; | |
return false; | |
} | |
// This is for either | |
public string Command(string commandString) | |
{ | |
commandString = NormalizeCommand(commandString); | |
// Are we executing a command? | |
if (executeCommandParser.Parse(commandString) != null) | |
{ | |
Do(commandString); | |
return OkResponse; | |
} | |
// Are we showing the values? | |
if (commandString == "show") | |
{ | |
return string.Join(Environment.NewLine, this.Select(x => $"{x.Key.ToUpper()}: {x.Value}")); | |
} | |
// Are we removing a value? | |
if (removeParser.Parse(commandString) != null) | |
{ | |
var name = removeParser.Parse(commandString); | |
Remove(name); | |
return OkResponse; | |
} | |
// Are we adjusting a value? | |
if (adjustParser.Parse(commandString) != null) | |
{ | |
var c = adjustParser.Parse(commandString); | |
Do($"{c.Operation} {c.Name} by {c.Value}"); | |
return OkResponse; | |
} | |
// Are we checking if something exists? | |
if (checkExistenceQuestionParser.Parse(commandString) != null || checkExistenceDeclarationParser.Parse(commandString) != null) | |
{ | |
return Ask(commandString).ToString(); | |
} | |
// Are we checking the value of something? | |
if (checkValueParser.Parse(commandString) != null) | |
{ | |
return Ask(commandString).ToString(); | |
} | |
// Are we retriving the value of something? | |
if (retrieveValueParser.Parse(commandString) != null) | |
{ | |
var value = retrieveValueParser.Parse(commandString); | |
if (!ContainsKey(value)) | |
{ | |
return "NULL"; | |
} | |
return this[value].ToString(); | |
} | |
return "ERROR"; | |
} | |
private string NormalizeCommand(string command) | |
{ | |
var allowCharacters = " _-".ToCharArray(); | |
command = command ?? string.Empty; | |
return new string(command.ToLower().Where(c => char.IsLetterOrDigit(c) || allowCharacters.Contains(c)).ToArray()); | |
} | |
// Lower case; Only letters, digits, or underscores | |
private string NormalizeVariableName(string name) | |
{ | |
return new string(name.ToLower().Where(c => | |
char.IsLetterOrDigit(c) || | |
c == '-' || | |
c == '_' | |
).ToArray()); | |
} | |
public class VariableOperation | |
{ | |
public string Name { get; set; } | |
public string Operation { get; set; } | |
public long Value { get; set; } | |
} | |
public class VariableComparison | |
{ | |
public string Name { get; set; } | |
public string Operator { get; set; } | |
public Bounding Value { get; set; } | |
} | |
public class VariableExistence | |
{ | |
public string Name { get; set; } | |
public bool ShouldExist { get; set; } | |
} | |
public struct Bounding | |
{ | |
public Bounding(long upper, long lower) | |
{ | |
Upper = upper; | |
Lower = lower; | |
} | |
public long Upper { get; set; } | |
public long Lower { get; set; } | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment