Skip to content

Instantly share code, notes, and snippets.

@deanebarker
Last active June 20, 2024 23:21
Show Gist options
  • Save deanebarker/befe90bc93a8d433cb4d7cc050592c95 to your computer and use it in GitHub Desktop.
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
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