1
0
mirror of https://github.com/chylex/Query.git synced 2025-07-04 21:38:53 +02:00

Compare commits

...

5 Commits

26 changed files with 554 additions and 588 deletions

View File

@ -7,7 +7,7 @@ public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUn
public NumberWithUnit VisitNumber(Expression.Number number) {
return new NumberWithUnit(number.NumberToken.Value, null);
}
public NumberWithUnit VisitNumbersWithUnits(Expression.NumbersWithUnits numbersWithUnits) {
NumberWithUnit result = new Number.Rational(0);
@ -17,24 +17,24 @@ public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUn
return result;
}
public NumberWithUnit VisitGrouping(Expression.Grouping grouping) {
return Evaluate(grouping.Expression);
}
public NumberWithUnit VisitUnary(Expression.Unary unary) {
(Token.Simple op, Expression right) = unary;
return op.Type switch {
SimpleTokenType.PLUS => +Evaluate(right),
SimpleTokenType.MINUS => -Evaluate(right),
_ => throw new CalculatorException("Unsupported unary operator: " + op.Type)
};
}
public NumberWithUnit VisitBinary(Expression.Binary binary) {
(Expression left, Token.Simple op, Expression right) = binary;
return op.Type switch {
SimpleTokenType.PLUS => Evaluate(left) + Evaluate(right),
SimpleTokenType.MINUS => Evaluate(left) - Evaluate(right),
@ -45,10 +45,10 @@ public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUn
_ => throw new CalculatorException("Unsupported binary operator: " + op.Type)
};
}
public NumberWithUnit VisitUnitAssignment(Expression.UnitAssignment unitAssignment) {
(Expression left, Unit right) = unitAssignment;
NumberWithUnit number = Evaluate(left);
if (number.Unit is null) {
@ -58,13 +58,13 @@ public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUn
throw new CalculatorException("Expression already has a unit, cannot assign a new unit: " + right);
}
}
public NumberWithUnit VisitUnitConversion(Expression.UnitConversion unitConversion) {
(Expression left, Unit unit) = unitConversion;
return Evaluate(left).ConvertTo(unit);
}
private NumberWithUnit Evaluate(Expression expression) {
return expression.Accept(this);
}

View File

@ -18,63 +18,77 @@ public abstract record Number : IAdditionOperators<Number, Number, Number>,
IAdditiveIdentity<Number, Number.Rational>,
IMultiplicativeIdentity<Number, Number.Rational> {
protected abstract decimal AsDecimal { get; }
public abstract Number Pow(Number exponent);
public abstract string ToString(IFormatProvider? formatProvider);
public sealed override string ToString() {
return ToString(CultureInfo.InvariantCulture);
}
/// <summary>
/// Represents an integer number with arbitrary precision.
/// </summary>
public sealed record Rational(BigRational Value) : Number {
protected override decimal AsDecimal => (decimal) Value;
public override Number Pow(Number exponent) {
if (exponent is Rational { Value: {} rationalExponent }) {
Fraction fractionExponent = rationalExponent.GetImproperFraction();
if (fractionExponent.Denominator == 1 && fractionExponent.Numerator >= 0) {
if (fractionExponent.Numerator >= 0 && fractionExponent.Denominator == 1) {
try {
return new Rational(BigRational.Pow(Value, fractionExponent.Numerator));
} catch (OverflowException) {}
}
if (fractionExponent.Numerator == 1 && fractionExponent.Denominator > 1) {
Number result = PowAsDecimal(exponent);
BigRational assumedPerfectPowerRoot = new BigRational(decimal.Floor(result.AsDecimal));
BigRational assumedPerfectPower = BigRational.Pow(assumedPerfectPowerRoot, fractionExponent.Denominator);
return assumedPerfectPower == Value ? assumedPerfectPowerRoot : result;
}
}
return PowAsDecimal(exponent);
Number PowAsDecimal(Number number) {
return new Decimal(AsDecimal).Pow(number);
}
return new Decimal(AsDecimal).Pow(exponent);
}
public override string ToString(IFormatProvider? formatProvider) {
Fraction fraction = Value.GetImproperFraction();
return fraction.Denominator == 1 ? fraction.Numerator.ToString(formatProvider) : AsDecimal.ToString(formatProvider);
}
}
/// <summary>
/// Represents a decimal number with limited precision.
/// </summary>
public sealed record Decimal(decimal Value) : Number {
public Decimal(double value) : this((decimal) value) {}
protected override decimal AsDecimal => Value;
public override Number Pow(Number exponent) {
double doubleValue = (double) Value;
double doubleExponent = (double) exponent.AsDecimal;
return new Decimal(System.Math.Pow(doubleValue, doubleExponent));
}
public override string ToString(IFormatProvider? formatProvider) {
return Value.ToString(formatProvider);
}
}
public static implicit operator Number(BigRational value) {
return new Rational(value);
}
public static implicit operator Number(int value) {
return new Rational(value);
}
@ -85,65 +99,65 @@ public abstract record Number : IAdditionOperators<Number, Number, Number>,
public static Rational AdditiveIdentity => new (BigInteger.Zero);
public static Rational MultiplicativeIdentity => new (BigInteger.One);
public static Number Add(Number left, Number right) {
return left + right;
}
public static Number Subtract(Number left, Number right) {
return left - right;
}
public static Number Multiply(Number left, Number right) {
return left * right;
}
public static Number Divide(Number left, Number right) {
return left / right;
}
public static Number Remainder(Number left, Number right) {
return left % right;
}
public static Number Pow(Number left, Number right) {
return left.Pow(right);
}
public static Number operator +(Number value) {
return value;
}
public static Number operator -(Number value) {
return Operate(value, BigRational.Negate, decimal.Negate);
}
public static Number operator +(Number left, Number right) {
return Operate(left, right, BigRational.Add, decimal.Add);
}
public static Number operator -(Number left, Number right) {
return Operate(left, right, BigRational.Subtract, decimal.Subtract);
}
public static Number operator *(Number left, Number right) {
return Operate(left, right, BigRational.Multiply, decimal.Multiply);
}
public static Number operator /(Number left, Number right) {
return Operate(left, right, BigRational.Divide, decimal.Divide);
}
public static Number operator %(Number left, Number right) {
return Operate(left, right, BigRational.Mod, decimal.Remainder);
}
private static Number Operate(Number value, Func<BigRational, BigRational> rationalOperation, Func<decimal, decimal> decimalOperation) {
return value is Rational rational
? new Rational(rationalOperation(rational.Value))
: new Decimal(decimalOperation(value.AsDecimal));
}
private static Number Operate(Number left, Number right, Func<BigRational, BigRational, BigRational> rationalOperation, Func<decimal, decimal, decimal> decimalOperation) {
return left is Rational leftRational && right is Rational rightRational
? new Rational(rationalOperation(leftRational.Value, rightRational.Value))

View File

@ -24,28 +24,28 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
throw new ArithmeticException("Cannot convert '" + Unit + "' to '" + targetUnit + "'");
}
}
public string ToString(IFormatProvider? formatProvider) {
string number = Number.ToString(formatProvider);
return Unit == null ? number : number + " " + Unit;
}
public override string ToString() {
return ToString(CultureInfo.InvariantCulture);
}
public static implicit operator NumberWithUnit(Number number) {
return new NumberWithUnit(number, null);
}
public static NumberWithUnit operator +(NumberWithUnit value) {
return value with { Number = +value.Number };
}
public static NumberWithUnit operator -(NumberWithUnit value) {
return value with { Number = -value.Number };
}
public static NumberWithUnit operator +(NumberWithUnit left, NumberWithUnit right) {
return Operate(left, right, Number.Add, static (leftNumber, leftUnit, rightNumber, rightUnit) => {
if (leftUnit == rightUnit) {
@ -59,7 +59,7 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
}
});
}
public static NumberWithUnit operator -(NumberWithUnit left, NumberWithUnit right) {
return Operate(left, right, Number.Subtract, static (leftNumber, leftUnit, rightNumber, rightUnit) => {
if (leftUnit == rightUnit) {
@ -73,23 +73,23 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
}
});
}
public static NumberWithUnit operator *(NumberWithUnit left, NumberWithUnit right) {
return OperateWithoutUnits(left, right, Number.Multiply, "Cannot multiply");
}
public static NumberWithUnit operator /(NumberWithUnit left, NumberWithUnit right) {
return OperateWithoutUnits(left, right, Number.Divide, "Cannot divide");
}
public static NumberWithUnit operator %(NumberWithUnit left, NumberWithUnit right) {
return OperateWithoutUnits(left, right, Number.Remainder, "Cannot modulo");
}
public NumberWithUnit Pow(NumberWithUnit exponent) {
return OperateWithoutUnits(this, exponent, Number.Pow, "Cannot exponentiate");
}
private static NumberWithUnit Operate(NumberWithUnit left, NumberWithUnit right, Func<Number, Number, Number> withoutUnitsOperation, Func<Number, Unit, Number, Unit, NumberWithUnit> withUnitsOperation) {
if (right.Unit is null) {
return left with { Number = withoutUnitsOperation(left.Number, right.Number) };
@ -101,7 +101,7 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
return withUnitsOperation(left.Number, left.Unit, right.Number, right.Unit);
}
}
private static NumberWithUnit OperateWithoutUnits(NumberWithUnit left, NumberWithUnit right, Func<Number, Number, Number> withoutUnitsOperation, string messagePrefix) {
return Operate(left, right, withoutUnitsOperation, (_, leftUnit, _, rightUnit) => throw new ArithmeticException(messagePrefix + " '" + leftUnit + "' and '" + rightUnit + "'"));
}

View File

@ -14,7 +14,7 @@ sealed class UnitUniverse(
FrozenDictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit
) {
public ImmutableArray<Unit> AllUnits => unitToConversionToPrimaryUnit.Keys;
internal bool TryConvert(Number value, Unit fromUnit, Unit toUnit, [NotNullWhen(true)] out Number? converted) {
if (fromUnit == toUnit) {
converted = value;
@ -29,7 +29,7 @@ sealed class UnitUniverse(
return false;
}
}
internal sealed record SI(string ShortPrefix, string LongPrefix, int Factor) {
internal static readonly List<SI> All = [
new SI("Q", "quetta", 30),
@ -58,58 +58,58 @@ sealed class UnitUniverse(
new SI("q", "quecto", -30)
];
}
internal sealed class Builder {
private readonly Unit primaryUnit;
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionToPrimaryUnit = new (ReferenceEqualityComparer.Instance);
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit = new (ReferenceEqualityComparer.Instance);
public Builder(Unit primaryUnit) {
this.primaryUnit = primaryUnit;
AddUnit(primaryUnit, 1);
}
public Builder AddUnit(Unit unit, Func<Number, Number> convertToPrimaryUnit, Func<Number, Number> convertFromPrimaryUnit) {
unitToConversionToPrimaryUnit.Add(unit, convertToPrimaryUnit);
unitToConversionFromPrimaryUnit.Add(unit, convertFromPrimaryUnit);
return this;
}
public Builder AddUnit(Unit unit, Number amountInPrimaryUnit) {
return AddUnit(unit, number => number * amountInPrimaryUnit, number => number / amountInPrimaryUnit);
}
private void AddUnitSI(SI si, Func<SI, Unit> unitFactory, Func<int, int> factorModifier) {
int factor = factorModifier(si.Factor);
BigInteger powerOfTen = BigInteger.Pow(10, System.Math.Abs(factor));
BigRational amountInPrimaryUnit = factor > 0 ? new BigRational(powerOfTen) : new BigRational(1, powerOfTen);
AddUnit(unitFactory(si), amountInPrimaryUnit);
}
public Builder AddSI(Func<SI, Unit> unitFactory, Func<int, int> factorModifier) {
foreach (SI si in SI.All) {
AddUnitSI(si, unitFactory, factorModifier);
}
return this;
}
public Builder AddSI(Func<int, int> factorModifier) {
Unit PrefixPrimaryUnit(SI si) {
return new Unit(si.ShortPrefix + primaryUnit.ShortName, [..primaryUnit.LongNames.Select(longName => si.LongPrefix + longName)]);
}
foreach (SI si in SI.All) {
AddUnitSI(si, PrefixPrimaryUnit, factorModifier);
}
return this;
}
public Builder AddSI() {
return AddSI(static factor => factor);
}
public UnitUniverse Build() {
return new UnitUniverse(
unitToConversionToPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance),

View File

@ -9,7 +9,7 @@ sealed class UnitUniverses {
private readonly FrozenDictionary<Unit, UnitUniverse> unitToUniverse;
public WordLookupTrieNode UnitLookupByWords { get; }
internal UnitUniverses(params UnitUniverse[] universes) {
Dictionary<Unit, UnitUniverse> unitToUniverseBuilder = new (ReferenceEqualityComparer.Instance);
WordLookupTrieNode.Builder unitLookupByWordsBuilder = new ();
@ -20,7 +20,7 @@ sealed class UnitUniverses {
unitLookupByWordsBuilder.Add(unit);
}
}
unitToUniverse = unitToUniverseBuilder.ToFrozenDictionary(ReferenceEqualityComparer.Instance);
UnitLookupByWords = unitLookupByWordsBuilder.Build();
}
@ -58,14 +58,14 @@ sealed class UnitUniverses {
private void Add(string name, Unit unit) {
Node node = root;
string[] words = name.Split(' ');
foreach (string word in words.AsSpan(..^1)) {
node = node.Child(word);
}
node.Children.Add(words[^1], Node.Create(unit));
}
public WordLookupTrieNode Build() {
return Build(root);
}

View File

@ -17,22 +17,22 @@ static class Units {
public static UnitUniverse Angle { get; } = AngleUniverse().Build();
public static UnitUniverse Temperature { get; } = TemperatureUniverse().Build();
public static UnitUniverse InformationEntropy { get; } = InformationEntropyUniverse().Build();
public static UnitUniverses All { get; } = new (Time, Length, Mass, Area, Volume, Angle, Temperature, InformationEntropy);
private static UnitUniverse.Builder TimeUniverse() {
var minute = 60;
var hour = minute * 60;
var day = hour * 24;
var week = day * 7;
return new UnitUniverse.Builder(new Unit("s", Pluralize("second")))
.AddUnit(new Unit("min", Pluralize("minute")), minute)
.AddUnit(new Unit("h", Pluralize("hour")), hour)
.AddUnit(new Unit("d", Pluralize("day")), day)
.AddUnit(new Unit("wk", Pluralize("week")), week);
}
private static UnitUniverse.Builder LengthUniverse() {
var inch = Parse("0", "0254");
var foot = inch * 12;
@ -41,7 +41,7 @@ static class Units {
var mile = yard * 1760;
var nauticalMile = 1_852;
var lightYear = 9_460_730_472_580_800;
return new UnitUniverse.Builder(new Unit("m", Pluralize("meter", "metre")))
.AddSI()
.AddUnit(new Unit("in", [ "inch", "inches", "\"" ]), inch)
@ -52,13 +52,13 @@ static class Units {
.AddUnit(new Unit("nmi", Pluralize("nautical mile")), nauticalMile)
.AddUnit(new Unit("ly", Pluralize("light-year", "light year")), lightYear);
}
private static UnitUniverse.Builder MassUniverse() {
var pound = Parse("453", "59237");
var stone = pound * 14;
var ounce = pound / 16;
var dram = ounce / 16;
return new UnitUniverse.Builder(new Unit("g", Pluralize("gram")))
.AddSI()
.AddUnit(new Unit("lb", [ "lbs", "pound", "pounds" ]), pound)
@ -66,7 +66,7 @@ static class Units {
.AddUnit(new Unit("oz", Pluralize("ounce")), ounce)
.AddUnit(new Unit("dr", Pluralize("dram")), dram);
}
private static UnitUniverse.Builder AreaUniverse() {
static Unit SquareMeter(string shortPrefix, string longPrefix) {
return new Unit(shortPrefix + "m2", [
@ -80,13 +80,13 @@ static class Units {
..Pluralize($"square {longPrefix}meter", $"square {longPrefix}metre")
]);
}
return new UnitUniverse.Builder(SquareMeter(string.Empty, string.Empty))
.AddSI(static si => SquareMeter(si.ShortPrefix, si.LongPrefix), static factor => factor * 2)
.AddUnit(new Unit("a", Pluralize("are")), 100)
.AddUnit(new Unit("ha", Pluralize("hectare")), 10_000);
}
private static UnitUniverse.Builder VolumeUniverse() {
static Unit CubicMeter(string shortPrefix, string longPrefix) {
return new Unit(shortPrefix + "m3", [
@ -99,31 +99,31 @@ static class Units {
..Pluralize($"cubic {longPrefix}meter", $"cubic {longPrefix}metre")
]);
}
return new UnitUniverse.Builder(new Unit("l", Pluralize("litre", "liter")))
.AddSI()
.AddUnit(CubicMeter(string.Empty, string.Empty), 1000)
.AddSI(static si => CubicMeter(si.ShortPrefix, si.LongPrefix), static factor => (factor * 3) + 3);
}
private static UnitUniverse.Builder AngleUniverse() {
return new UnitUniverse.Builder(new Unit("deg", [ "°", "degree", "degrees" ]))
.AddUnit(new Unit("rad", Pluralize("radian")), new Number.Decimal((decimal) System.Math.PI / 180M))
.AddUnit(new Unit("grad", Pluralize("gradian", "grade", "gon")), Ratio(9, 10));
}
private static BigRational KelvinOffset { get; } = Parse("273", "15");
private static UnitUniverse.Builder TemperatureUniverse() {
return new UnitUniverse.Builder(new Unit("°C", [ "C", "Celsius", "celsius" ]))
.AddUnit(new Unit("°F", [ "F", "Fahrenheit", "fahrenheit" ]), static f => (f - 32) * Ratio(5, 9), static c => c * Ratio(9, 5) + 32)
.AddUnit(new Unit("K", [ "Kelvin", "kelvin" ]), static k => k - KelvinOffset, static c => c + KelvinOffset);
}
private static UnitUniverse.Builder InformationEntropyUniverse() {
var bit = Ratio(1, 8);
var nibble = bit * 4;
return new UnitUniverse.Builder(new Unit("B", Pluralize("byte")))
.AddSI()
.AddUnit(new Unit("b", Pluralize("bit")), bit)
@ -137,19 +137,19 @@ static class Units {
.AddUnit(new Unit("ZiB", Pluralize("zebibyte")), Pow(1024, 7))
.AddUnit(new Unit("YiB", Pluralize("yobibyte")), Pow(1024, 8));
}
private static BigRational Parse(string integerPart, string fractionalPart) {
return Tokenizer.ParseNumber(integerPart, fractionalPart);
}
private static BigRational Ratio(long numerator, long denominator) {
return new BigRational(numerator, denominator);
}
private static BigRational Pow(int value, int exponent) {
return BigRational.Pow(value, exponent);
}
private static ImmutableArray<string> Pluralize(params string[] names) {
return [..names.SelectMany(static name => new [] { name, name + "s" })];
}

View File

@ -6,9 +6,9 @@ namespace Calculator.Parser;
public abstract record Expression {
private Expression() {}
public abstract T Accept<T>(ExpressionVisitor<T> visitor);
public sealed record Number(Token.Number NumberToken) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitNumber(this);
@ -19,7 +19,7 @@ public abstract record Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitNumbersWithUnits(this);
}
public override string ToString() {
return nameof(NumbersWithUnits) + " { " + string.Join(", ", NumberTokensWithUnits.Select(static (number, unit) => number + " " + unit)) + " }";
}
@ -30,25 +30,25 @@ public abstract record Expression {
return visitor.VisitGrouping(this);
}
}
public sealed record Unary(Token.Simple Operator, Expression Right) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitUnary(this);
}
}
public sealed record Binary(Expression Left, Token.Simple Operator, Expression Right) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitBinary(this);
}
}
public sealed record UnitAssignment(Expression Left, Unit Unit) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitUnitAssignment(this);
}
}
public sealed record UnitConversion(Expression Left, Unit Unit) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitUnitConversion(this);

View File

@ -2,16 +2,16 @@
public interface ExpressionVisitor<T> {
T VisitNumber(Expression.Number number);
T VisitNumbersWithUnits(Expression.NumbersWithUnits numbersWithUnits);
T VisitGrouping(Expression.Grouping grouping);
T VisitUnary(Expression.Unary unary);
T VisitBinary(Expression.Binary binary);
T VisitUnitAssignment(Expression.UnitAssignment unitAssignment);
T VisitUnitConversion(Expression.UnitConversion unitConversion);
}

View File

@ -7,94 +7,94 @@ namespace Calculator.Parser;
public sealed class Parser(ImmutableArray<Token> tokens) {
private int current = 0;
private bool IsEOF => current >= tokens.Length;
private static readonly ImmutableArray<SimpleTokenType> PLUS_MINUS = [
SimpleTokenType.PLUS,
SimpleTokenType.MINUS
];
private static readonly ImmutableArray<SimpleTokenType> STAR_SLASH_PERCENT = [
SimpleTokenType.STAR,
SimpleTokenType.SLASH,
SimpleTokenType.PERCENT
];
private static readonly ImmutableArray<SimpleTokenType> CARET = [
SimpleTokenType.CARET
];
private bool Match(SimpleTokenType expectedTokenType, [NotNullWhen(true)] out Token.Simple? token) {
return Match(simpleToken => simpleToken.Type == expectedTokenType, out token);
}
private bool Match(ImmutableArray<SimpleTokenType> expectedTokenTypes, [NotNullWhen(true)] out Token.Simple? token) {
return Match(simpleToken => expectedTokenTypes.Contains(simpleToken.Type), out token);
}
private bool Match<T>([NotNullWhen(true)] out T? token) where T : Token {
return Match(static _ => true, out token);
}
private bool Match<T>(Predicate<T> predicate, [NotNullWhen(true)] out T? token) where T : Token {
if (!IsEOF && tokens[current] is T t && predicate(t)) {
current++;
token = t;
return true;
}
token = null;
return false;
}
public Expression Parse() {
Expression term = Term();
if (!IsEOF) {
throw new ParseException("Incomplete expression");
}
return term;
}
private Expression Term() {
return Binary(Factor, PLUS_MINUS);
}
private Expression Factor() {
return Binary(Exponentiation, STAR_SLASH_PERCENT);
}
private Expression Exponentiation() {
return Binary(Conversion, CARET);
}
private Expression Binary(Func<Expression> term, ImmutableArray<SimpleTokenType> expectedTokenTypes) {
Expression left = term();
while (Match(expectedTokenTypes, out Token.Simple? op)) {
Expression right = term();
left = new Expression.Binary(left, op, right);
}
return left;
}
private Expression Conversion() {
Expression left = Unary();
while (MatchUnitConversionOperator()) {
if (!MatchUnit(out Unit? unit)) {
throw new ParseException("Expected a unit literal");
}
left = new Expression.UnitConversion(left, unit);
}
return left;
}
private Expression Unary() {
if (Match(PLUS_MINUS, out Token.Simple? op)) {
Expression right = Unary();
@ -104,56 +104,56 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
return Primary();
}
}
private Expression Primary() {
if (Match(LiteralPredicate, out Token? literal)) {
if (literal is not Token.Number number) {
throw new ParseException("Expected a number literal");
}
Expression expression;
if (!MatchUnit(out Unit? unit)) {
expression = new Expression.Number(number);
}
else {
var numbersWithUnits = ImmutableArray.CreateBuilder<(Token.Number, Unit)>();
numbersWithUnits.Add((number, unit));
while (MatchNumberWithUnit(out var numberWithUnit)) {
numbersWithUnits.Add(numberWithUnit.Value);
}
expression = new Expression.NumbersWithUnits(numbersWithUnits.ToImmutable());
}
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
expression = new Expression.Binary(expression, new Token.Simple(SimpleTokenType.STAR), InsideParentheses());
}
return expression;
}
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
return new Expression.Grouping(InsideParentheses());
}
throw new ParseException("Unexpected token type: " + tokens[current]);
}
private static bool LiteralPredicate(Token token) {
return token is Token.Text or Token.Number;
}
private Expression InsideParentheses() {
Expression term = Term();
if (!Match(SimpleTokenType.RIGHT_PARENTHESIS, out _)) {
throw new ParseException("Expected ')' after expression.");
}
int position = current;
if (MatchUnitConversionOperator()) {
if (MatchUnit(out Unit? toUnit)) {
return new Expression.UnitConversion(term, toUnit);
@ -162,7 +162,7 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
current = position;
}
}
if (MatchUnit(out Unit? unit)) {
return new Expression.UnitAssignment(term, unit);
}
@ -170,33 +170,33 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
return term;
}
}
private bool MatchNumberWithUnit([NotNullWhen(true)] out (Token.Number, Unit)? numberWithUnit) {
if (!Match(out Token.Number? number)) {
numberWithUnit = null;
return false;
}
if (!MatchUnit(out Unit? unit)) {
throw new ParseException("Expected a unit literal");
}
numberWithUnit = (number, unit);
return true;
}
private bool MatchUnit([NotNullWhen(true)] out Unit? unit) {
int position = current;
UnitUniverses.WordLookupTrieNode node = Units.All.UnitLookupByWords;
// ReSharper disable once AccessToModifiedClosure
while (Match(token => node.Children.ContainsKey(token.Value), out Token.Text? text)) {
node = node.Children[text.Value];
}
unit = node.Unit;
if (unit != null) {
return true;
}
@ -205,7 +205,7 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
return false;
}
}
private bool MatchUnitConversionOperator() {
return Match<Token.Text>(static text => text.Value is "to" or "in", out _);
}

View File

@ -7,7 +7,7 @@ public enum SimpleTokenType {
SLASH,
PERCENT,
CARET,
LEFT_PARENTHESIS,
RIGHT_PARENTHESIS
}

View File

@ -5,7 +5,7 @@ namespace Calculator.Parser;
public abstract record Token {
private Token() {}
public sealed record Simple(SimpleTokenType Type) : Token {
#pragma warning disable CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
public override string ToString() {
@ -23,13 +23,13 @@ public abstract record Token {
}
#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
}
public sealed record Text(string Value) : Token {
public override string ToString() {
return Value;
}
}
public sealed record Number(Math.Number Value) : Token {
public override string ToString() {
return Value.ToString(CultureInfo.InvariantCulture);

View File

@ -9,114 +9,114 @@ namespace Calculator.Parser;
public sealed class Tokenizer(string input) {
private int position = 0;
private bool IsEOF => position >= input.Length;
private char Advance() {
return input[position++];
}
private bool Match(char c) {
return Match(found => found == c, out _);
}
private bool Match(Predicate<char> predicate, out char c) {
if (IsEOF) {
c = default;
return false;
}
c = input[position];
if (!predicate(c)) {
return false;
}
position++;
return true;
}
private void MatchWhile(StringBuilder result, Predicate<char> predicate) {
while (Match(predicate, out char c)) {
result.Append(c);
}
}
private string MatchWhile(Predicate<char> predicate) {
var result = new StringBuilder();
MatchWhile(result, predicate);
return result.ToString();
}
private string MatchRest(char firstChar, Predicate<char> predicate) {
var result = new StringBuilder();
result.Append(firstChar);
MatchWhile(result, predicate);
return result.ToString();
}
public ImmutableArray<Token> Scan() {
ImmutableArray<Token>.Builder tokens = ImmutableArray.CreateBuilder<Token>();
void AddToken(Token token) {
tokens.Add(token);
}
void AddSimpleToken(SimpleTokenType tokenType) {
AddToken(new Token.Simple(tokenType));
}
while (!IsEOF) {
char c = Advance();
switch (c) {
case ' ':
// Ignore whitespace.
break;
case '+':
AddSimpleToken(SimpleTokenType.PLUS);
break;
case '-':
AddSimpleToken(SimpleTokenType.MINUS);
break;
case '*':
AddSimpleToken(SimpleTokenType.STAR);
break;
case '/':
AddSimpleToken(SimpleTokenType.SLASH);
break;
case '%':
AddSimpleToken(SimpleTokenType.PERCENT);
break;
case '^':
AddSimpleToken(SimpleTokenType.CARET);
break;
case '(':
AddSimpleToken(SimpleTokenType.LEFT_PARENTHESIS);
break;
case ')':
AddSimpleToken(SimpleTokenType.RIGHT_PARENTHESIS);
break;
case '"' or '\'':
AddToken(new Token.Text(c.ToString()));
break;
case '°':
case {} when char.IsLetter(c):
AddToken(new Token.Text(MatchRest(c, char.IsLetterOrDigit)));
break;
case {} when char.IsAsciiDigit(c):
string integerPart = MatchRest(c, char.IsAsciiDigit);
if (Match('.')) {
string fractionalPart = MatchWhile(char.IsAsciiDigit);
AddToken(new Token.Number(ParseNumber(integerPart, fractionalPart)));
@ -124,17 +124,17 @@ public sealed class Tokenizer(string input) {
else {
AddToken(new Token.Number(ParseNumber(integerPart)));
}
break;
default:
throw new TokenizationException("Unexpected character: " + c, c);
}
}
return tokens.ToImmutable();
}
internal static BigRational ParseNumber(string integerPart, string? fractionalPart = null) {
if (fractionalPart == null) {
return new BigRational(BigInteger.Parse(integerPart, NumberStyles.Integer, CultureInfo.InvariantCulture));

View File

@ -11,7 +11,7 @@ sealed class CalculatorApp : IApp {
ImmutableArray<Token> tokens = new Tokenizer(command).Scan();
Expression expression = new Parser(tokens).Parse();
NumberWithUnit result = expression.Accept(new CalculatorExpressionVisitor());
output = result.ToString();
return true;
}

View File

@ -14,11 +14,11 @@ sealed class KillProcessApp : IApp {
}
int succeeded = 0, failed = 0;
foreach (string processName in args[1..]) {
try {
Process[] processes = Process.GetProcessesByName(processName.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase) ? processName[..^4] : processName);
foreach (Process process in processes) {
try {
process.Kill();
@ -26,21 +26,21 @@ sealed class KillProcessApp : IApp {
} catch {
++failed;
}
process.Close();
}
} catch {
++failed;
}
}
var build = new StringBuilder();
build.Append("Killed ").Append(succeeded).Append(" process").Append(succeeded == 1 ? "" : "es");
if (failed > 0) {
build.Append(", failed ").Append(failed);
}
output = build.Append('.').ToString();
return true;
}

View File

@ -10,7 +10,7 @@ sealed class MemeApp : IApp {
{ "flip", @"(╯°□°)╯︵ ┻━┻" },
{ "tableflip", @"(╯°□°)╯︵ ┻━┻" }
};
public bool TryRun(string command, [NotNullWhen(true)] out string? output) {
return Map.TryGetValue(command, out output);
}

View File

@ -5,19 +5,19 @@ namespace Query.Command;
sealed class CommandHistory {
private readonly List<string> queries = [];
private readonly List<string> results = [];
public IList<string> Queries => queries;
public IList<string> Results => results;
public void AddQuery(string text) {
queries.Add(text);
}
public void AddResult(string text) {
results.Add(text);
}
public void Clear() {
queries.Clear();
results.Clear();

View File

@ -10,7 +10,7 @@ sealed class CommandProcessor {
new KillProcessApp(),
new CalculatorApp()
];
public string Run(string command) {
try {
foreach (IApp app in apps) {
@ -18,7 +18,7 @@ sealed class CommandProcessor {
return output;
}
}
return "Unknown command.";
} catch (Exception e) {
throw new CommandException(e.Message, e);

View File

@ -6,57 +6,57 @@ namespace Query.Form;
sealed class KeyboardHook {
public event EventHandler? Triggered;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly NativeMethods.HookProc keyboardHookDelegate;
private IntPtr keyboardHook;
public KeyboardHook() {
keyboardHookDelegate = KeyboardHookProc;
}
public void StartHook() {
if (keyboardHook != IntPtr.Zero) {
NativeMethods.UnhookWindowsHookEx(keyboardHook);
}
keyboardHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_KEYBOARD_LL, keyboardHookDelegate, IntPtr.Zero, 0);
}
public void StopHook() {
if (keyboardHook != IntPtr.Zero) {
NativeMethods.UnhookWindowsHookEx(keyboardHook);
keyboardHook = IntPtr.Zero;
}
}
private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam) {
if (wParam == NativeMethods.WM_KEYDOWN) {
Keys key = (Keys) Marshal.ReadInt32(lParam);
if (key is Keys.LWin or Keys.RWin && Control.ModifierKeys.HasFlag(Keys.Control)) {
Triggered?.Invoke(this, EventArgs.Empty);
return NativeMethods.HookHandled;
}
}
return NativeMethods.CallNextHookEx(keyboardHook, nCode, wParam, lParam);
}
private static class NativeMethods {
public const int WH_KEYBOARD_LL = 13;
public const int WM_KEYDOWN = 0x0100;
public static readonly IntPtr HookHandled = new (-1);
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);
[DllImport("user32.dll")]
public static extern bool UnhookWindowsHookEx(IntPtr idHook);
[DllImport("user32.dll")]
public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);
}

View File

@ -1,135 +1,133 @@
namespace Query.Form {
sealed partial class MainForm {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
queryLog = new Query.Form.QueryHistoryLog();
queryBox = new Query.Form.QueryTextBox();
trayIcon = new System.Windows.Forms.NotifyIcon(components);
contextMenuTray = new System.Windows.Forms.ContextMenuStrip(components);
showToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
hookToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
contextMenuTray.SuspendLayout();
SuspendLayout();
//
// queryLog
//
queryLog.Anchor = ((System.Windows.Forms.AnchorStyles) (((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right));
queryLog.BackColor = System.Drawing.Color.Transparent;
queryLog.Font = new System.Drawing.Font("Consolas", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) 238));
queryLog.Location = new System.Drawing.Point(7, 8);
queryLog.Margin = new System.Windows.Forms.Padding(7, 8, 7, 8);
queryLog.Name = "queryLog";
queryLog.Size = new System.Drawing.Size(696, 296);
queryLog.TabIndex = 1;
//
// queryBox
//
queryBox.BackColor = System.Drawing.Color.FromArgb(((int) ((byte) 36)), ((int) ((byte) 36)), ((int) ((byte) 36)));
queryBox.Dock = System.Windows.Forms.DockStyle.Bottom;
queryBox.Location = new System.Drawing.Point(0, 312);
queryBox.Margin = new System.Windows.Forms.Padding(0);
queryBox.Name = "queryBox";
queryBox.Size = new System.Drawing.Size(709, 50);
queryBox.TabIndex = 0;
//
// trayIcon
//
trayIcon.ContextMenuStrip = contextMenuTray;
trayIcon.Icon = ((System.Drawing.Icon) resources.GetObject("trayIcon.Icon"));
trayIcon.Text = "Query";
trayIcon.Visible = true;
trayIcon.Click += trayIcon_Click;
//
// contextMenuTray
//
contextMenuTray.ImageScalingSize = new System.Drawing.Size(20, 20);
contextMenuTray.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
showToolStripMenuItem, hookToolStripMenuItem, exitToolStripMenuItem
});
contextMenuTray.Name = "contextMenuTray";
contextMenuTray.ShowImageMargin = false;
contextMenuTray.ShowItemToolTips = false;
contextMenuTray.Size = new System.Drawing.Size(90, 76);
//
// showToolStripMenuItem
//
showToolStripMenuItem.Name = "showToolStripMenuItem";
showToolStripMenuItem.Size = new System.Drawing.Size(89, 24);
showToolStripMenuItem.Text = "Show";
showToolStripMenuItem.Click += showToolStripMenuItem_Click;
//
// hookToolStripMenuItem
//
hookToolStripMenuItem.Name = "hookToolStripMenuItem";
hookToolStripMenuItem.Size = new System.Drawing.Size(89, 24);
hookToolStripMenuItem.Text = "Hook";
hookToolStripMenuItem.Click += hookToolStripMenuItem_Click;
//
// exitToolStripMenuItem
//
exitToolStripMenuItem.Name = "exitToolStripMenuItem";
exitToolStripMenuItem.Size = new System.Drawing.Size(89, 24);
exitToolStripMenuItem.Text = "Exit";
exitToolStripMenuItem.Click += exitToolStripMenuItem_Click;
//
// MainForm
//
AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
BackColor = System.Drawing.Color.FromArgb(((int) ((byte) 64)), ((int) ((byte) 64)), ((int) ((byte) 64)));
ClientSize = new System.Drawing.Size(709, 362);
ControlBox = false;
Controls.Add(queryLog);
Controls.Add(queryBox);
FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
Icon = ((System.Drawing.Icon) resources.GetObject("$this.Icon"));
Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
MaximizeBox = false;
MinimizeBox = false;
ShowInTaskbar = false;
Text = "Query";
Deactivate += MainForm_Deactivate;
Shown += MainForm_Shown;
LocationChanged += MainForm_LocationChanged;
SizeChanged += MainForm_SizeChanged;
contextMenuTray.ResumeLayout(false);
ResumeLayout(false);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
this.components = new System.ComponentModel.Container();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
this.queryLog = new QueryHistoryLog();
this.queryBox = new QueryTextBox();
this.trayIcon = new System.Windows.Forms.NotifyIcon(this.components);
this.contextMenuTray = new System.Windows.Forms.ContextMenuStrip(this.components);
this.showToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.hookToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exitToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.contextMenuTray.SuspendLayout();
this.SuspendLayout();
//
// queryLog
//
this.queryLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.queryLog.BackColor = System.Drawing.Color.Transparent;
this.queryLog.Font = new System.Drawing.Font("Consolas", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.queryLog.Location = new System.Drawing.Point(5, 5);
this.queryLog.Margin = new System.Windows.Forms.Padding(5);
this.queryLog.Name = "queryLog";
this.queryLog.Size = new System.Drawing.Size(522, 192);
this.queryLog.TabIndex = 1;
//
// queryBox
//
this.queryBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.queryBox.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(36)))), ((int)(((byte)(36)))), ((int)(((byte)(36)))));
this.queryBox.Location = new System.Drawing.Point(0, 202);
this.queryBox.Margin = new System.Windows.Forms.Padding(0);
this.queryBox.Name = "queryBox";
this.queryBox.Size = new System.Drawing.Size(532, 33);
this.queryBox.TabIndex = 0;
//
// trayIcon
//
this.trayIcon.ContextMenuStrip = this.contextMenuTray;
this.trayIcon.Icon = ((System.Drawing.Icon)(resources.GetObject("trayIcon.Icon")));
this.trayIcon.Text = "Query";
this.trayIcon.Visible = true;
this.trayIcon.Click += new System.EventHandler(this.trayIcon_Click);
//
// contextMenuTray
//
this.contextMenuTray.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.showToolStripMenuItem,
this.hookToolStripMenuItem,
this.exitToolStripMenuItem});
this.contextMenuTray.Name = "contextMenuTray";
this.contextMenuTray.ShowImageMargin = false;
this.contextMenuTray.ShowItemToolTips = false;
this.contextMenuTray.Size = new System.Drawing.Size(128, 92);
//
// showToolStripMenuItem
//
this.showToolStripMenuItem.Name = "showToolStripMenuItem";
this.showToolStripMenuItem.Size = new System.Drawing.Size(127, 22);
this.showToolStripMenuItem.Text = "Show";
this.showToolStripMenuItem.Click += new System.EventHandler(this.showToolStripMenuItem_Click);
//
// hookToolStripMenuItem
//
this.hookToolStripMenuItem.Name = "hookToolStripMenuItem";
this.hookToolStripMenuItem.Size = new System.Drawing.Size(127, 22);
this.hookToolStripMenuItem.Text = "Hook";
this.hookToolStripMenuItem.Click += new System.EventHandler(this.hookToolStripMenuItem_Click);
//
// exitToolStripMenuItem
//
this.exitToolStripMenuItem.Name = "exitToolStripMenuItem";
this.exitToolStripMenuItem.Size = new System.Drawing.Size(127, 22);
this.exitToolStripMenuItem.Text = "Exit";
this.exitToolStripMenuItem.Click += new System.EventHandler(this.exitToolStripMenuItem_Click);
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(64)))), ((int)(((byte)(64)))));
this.ClientSize = new System.Drawing.Size(532, 235);
this.ControlBox = false;
this.Controls.Add(this.queryLog);
this.Controls.Add(this.queryBox);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "MainForm";
this.ShowInTaskbar = false;
this.Text = "Query";
this.Deactivate += new System.EventHandler(this.MainForm_Deactivate);
this.Shown += new System.EventHandler(this.MainForm_Shown);
this.contextMenuTray.ResumeLayout(false);
this.ResumeLayout(false);
#endregion
}
#endregion
private QueryTextBox queryBox;
private QueryHistoryLog queryLog;
private System.Windows.Forms.NotifyIcon trayIcon;
private System.Windows.Forms.ContextMenuStrip contextMenuTray;
private System.Windows.Forms.ToolStripMenuItem showToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem hookToolStripMenuItem;
}
}
private Query.Form.QueryTextBox queryBox;
private Query.Form.QueryHistoryLog queryLog;
private System.Windows.Forms.NotifyIcon trayIcon;
private System.Windows.Forms.ContextMenuStrip contextMenuTray;
private System.Windows.Forms.ToolStripMenuItem showToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem exitToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem hookToolStripMenuItem;
}
}

View File

@ -8,33 +8,33 @@ namespace Query.Form;
sealed partial class MainForm : System.Windows.Forms.Form {
private readonly CommandProcessor processor;
private readonly CommandHistory history;
private readonly Timer focusTimer;
private readonly KeyboardHook keyboardHook;
private bool isLoaded;
public MainForm() {
InitializeComponent();
processor = new CommandProcessor();
history = new CommandHistory();
queryBox.Setup(history, str => queryLog.AddEntry(str, QueryHistoryLog.EntryType.Information));
keyboardHook = new KeyboardHook();
keyboardHook.Triggered += keyboardHook_Triggered;
focusTimer = new Timer {
Interval = 1
};
focusTimer.Tick += focusTimer_Tick;
Disposed += MainForm_Disposed;
queryBox.CommandRan += queryBox_CommandRan;
}
private void SetShown(bool show) {
if (show) {
focusTimer.Start();
@ -43,63 +43,77 @@ sealed partial class MainForm : System.Windows.Forms.Form {
Hide();
}
}
private void MainForm_Shown(object? sender, EventArgs e) {
if (Screen.PrimaryScreen is {} primaryScreen) {
Rectangle screenRect = primaryScreen.WorkingArea;
Location = new Point(screenRect.X + screenRect.Width - Width, screenRect.Y + screenRect.Height - Height);
}
FixLocation();
if (!isLoaded) {
isLoaded = true;
keyboardHook.StartHook();
}
}
private void MainForm_LocationChanged(object sender, EventArgs e) {
FixLocation();
}
private void MainForm_SizeChanged(object sender, EventArgs e) {
FixLocation();
}
private bool isMoving = false;
private void FixLocation() {
if (!isMoving && Screen.FromControl(this) is {} screen) {
Rectangle screenRect = screen.WorkingArea;
isMoving = true;
Location = new Point(screenRect.X + screenRect.Width - Width, screenRect.Y + screenRect.Height - Height);
isMoving = false;
}
}
private void MainForm_Deactivate(object? sender, EventArgs e) {
SetShown(false);
}
private void MainForm_Disposed(object? sender, EventArgs e) {
keyboardHook.StopHook();
}
private void trayIcon_Click(object? sender, EventArgs e) {
if (((MouseEventArgs) e).Button == MouseButtons.Left) {
SetShown(true);
}
}
private void showToolStripMenuItem_Click(object? sender, EventArgs e) {
SetShown(true);
}
private void hookToolStripMenuItem_Click(object? sender, EventArgs e) {
keyboardHook.StopHook();
keyboardHook.StartHook();
}
private void exitToolStripMenuItem_Click(object? sender, EventArgs e) {
Application.Exit();
}
private void keyboardHook_Triggered(object? sender, EventArgs e) {
SetShown(!Visible);
}
private void focusTimer_Tick(object? sender, EventArgs e) {
WindowState = FormWindowState.Minimized;
Show();
Activate();
WindowState = FormWindowState.Normal;
queryBox.Focus();
focusTimer.Stop();
}
private void queryBox_CommandRan(object? sender, CommandEventArgs e) {
string command = e.Command;
if (command is "exit" or "quit") {
Application.Exit();
}
@ -113,16 +127,16 @@ sealed partial class MainForm : System.Windows.Forms.Form {
else {
try {
string result = processor.Run(command);
queryLog.AddEntry("> " + command, QueryHistoryLog.EntryType.UserInput);
history.AddQuery(command);
queryLog.AddEntry(result, QueryHistoryLog.EntryType.CommandResult);
history.AddResult(result);
} catch (CommandException ex) {
queryLog.AddEntry("> " + command, QueryHistoryLog.EntryType.UserInput);
history.AddQuery(command);
queryLog.AddEntry(ex.Message, QueryHistoryLog.EntryType.Error);
}
}

View File

@ -1,64 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@ -112,19 +53,19 @@
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="trayIcon.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<metadata name="trayIcon.TrayLocation" type="System.Drawing.Point, System.Drawing.Primitives, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="contextMenuTray.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<metadata name="contextMenuTray.TrayLocation" type="System.Drawing.Point, System.Drawing.Primitives, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>112, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="trayIcon.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<assembly alias="System.Drawing.Common" name="System.Drawing.Common, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51" />
<data name="trayIcon.Icon" type="System.Drawing.Icon, System.Drawing.Common" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
AAABAAEAGBgAAAEAIACICQAAFgAAACgAAAAYAAAAMAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAkJCT/JCQk/yQkJP8kJCT/JCQk/yQkJP8kJCT/JCQk/yQkJP8kJCT/JCQk/yQkJP8kJCT/JCQk/yQk
@ -170,7 +111,7 @@
AEE=
</value>
</data>
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing.Common" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
AAABAAEAGBgAAAEAIACICQAAFgAAACgAAAAYAAAAMAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAkJCT/JCQk/yQkJP8kJCT/JCQk/yQkJP8kJCT/JCQk/yQkJP8kJCT/JCQk/yQkJP8kJCT/JCQk/yQk

View File

@ -1,57 +1,58 @@
namespace Query.Form {
partial class QueryHistoryLog {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
partial class QueryHistoryLog {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
container = new System.Windows.Forms.FlowLayoutPanel();
SuspendLayout();
//
// container
//
container.AutoScroll = true;
container.Dock = System.Windows.Forms.DockStyle.Fill;
container.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
container.ForeColor = System.Drawing.Color.FromArgb(((int) ((byte) 240)), ((int) ((byte) 240)), ((int) ((byte) 240)));
container.Location = new System.Drawing.Point(0, 0);
container.Margin = new System.Windows.Forms.Padding(4);
container.Name = "container";
container.Size = new System.Drawing.Size(195, 191);
container.TabIndex = 0;
container.WrapContents = false;
//
// QueryHistoryLog
//
AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
BackColor = System.Drawing.Color.Transparent;
Controls.Add(container);
Font = new System.Drawing.Font("Consolas", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) 238));
Margin = new System.Windows.Forms.Padding(4);
Size = new System.Drawing.Size(195, 191);
ResumeLayout(false);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
this.container = new System.Windows.Forms.FlowLayoutPanel();
this.SuspendLayout();
//
// container
//
this.container.AutoScroll = true;
this.container.Dock = System.Windows.Forms.DockStyle.Fill;
this.container.FlowDirection = System.Windows.Forms.FlowDirection.TopDown;
this.container.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(240)))), ((int)(((byte)(240)))), ((int)(((byte)(240)))));
this.container.Location = new System.Drawing.Point(0, 0);
this.container.Name = "container";
this.container.Size = new System.Drawing.Size(150, 150);
this.container.TabIndex = 0;
this.container.WrapContents = false;
//
// QueryHistoryLog
//
this.AutoScaleDimensions = new System.Drawing.SizeF(10F, 22F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.Transparent;
this.Controls.Add(this.container);
this.Font = new System.Drawing.Font("Consolas", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.Name = "QueryHistoryLog";
this.ResumeLayout(false);
#endregion
}
#endregion
private System.Windows.Forms.FlowLayoutPanel container;
}
private System.Windows.Forms.FlowLayoutPanel container;
}
}

View File

@ -11,21 +11,21 @@ sealed partial class QueryHistoryLog : UserControl {
Information,
Error
}
private static readonly Dictionary<EntryType, Color> EntryColorMap = new () {
{ EntryType.UserInput, Color.FromArgb(160, 160, 160) },
{ EntryType.CommandResult, Color.FromArgb(240, 240, 240) },
{ EntryType.Information, Color.FromArgb(160, 255, 140) },
{ EntryType.Error, Color.FromArgb(255, 40, 40) }
};
public QueryHistoryLog() {
InitializeComponent();
}
public void AddEntry(string text, EntryType type) {
int width = container.Width - SystemInformation.VerticalScrollBarWidth;
Label label = new Label {
AutoSize = true,
Font = container.Font,
@ -35,11 +35,11 @@ sealed partial class QueryHistoryLog : UserControl {
MaximumSize = new Size(width, 0),
Width = width
};
container.Controls.Add(label);
container.AutoScrollPosition = new Point(0, container.VerticalScroll.Maximum);
}
public void ClearEntries() {
container.Controls.Clear();
}

View File

@ -1,61 +1,58 @@
namespace Query.Form {
partial class QueryTextBox {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
partial class QueryTextBox {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) {
if (disposing && (components != null)) {
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
tb = new Query.Form.QueryTextBox.CustomTextBox();
SuspendLayout();
//
// tb
//
tb.Anchor = ((System.Windows.Forms.AnchorStyles) (((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right));
tb.BackColor = System.Drawing.Color.FromArgb(((int) ((byte) 36)), ((int) ((byte) 36)), ((int) ((byte) 36)));
tb.BorderStyle = System.Windows.Forms.BorderStyle.None;
tb.Font = new System.Drawing.Font("Consolas", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte) 238));
tb.ForeColor = System.Drawing.Color.FromArgb(((int) ((byte) 240)), ((int) ((byte) 240)), ((int) ((byte) 240)));
tb.Location = new System.Drawing.Point(11, 11);
tb.Margin = new System.Windows.Forms.Padding(11);
tb.Name = "tb";
tb.Size = new System.Drawing.Size(670, 28);
tb.TabIndex = 0;
//
// QueryTextBox
//
AutoScaleDimensions = new System.Drawing.SizeF(8F, 20F);
AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
BackColor = System.Drawing.Color.FromArgb(((int) ((byte) 36)), ((int) ((byte) 36)), ((int) ((byte) 36)));
Controls.Add(tb);
Margin = new System.Windows.Forms.Padding(4, 5, 4, 5);
Size = new System.Drawing.Size(692, 50);
ResumeLayout(false);
PerformLayout();
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() {
this.tb = new CustomTextBox();
this.SuspendLayout();
//
// tb
//
this.tb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.tb.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(36)))), ((int)(((byte)(36)))), ((int)(((byte)(36)))));
this.tb.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.tb.Font = new System.Drawing.Font("Consolas", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(238)));
this.tb.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(240)))), ((int)(((byte)(240)))), ((int)(((byte)(240)))));
this.tb.Location = new System.Drawing.Point(5, 6);
this.tb.Margin = new System.Windows.Forms.Padding(5);
this.tb.Name = "tb";
this.tb.Size = new System.Drawing.Size(509, 23);
this.tb.TabIndex = 0;
//
// QueryTextBox
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(36)))), ((int)(((byte)(36)))), ((int)(((byte)(36)))));
this.Controls.Add(this.tb);
this.Name = "QueryTextBox";
this.Size = new System.Drawing.Size(519, 33);
this.ResumeLayout(false);
this.PerformLayout();
#endregion
}
#endregion
private CustomTextBox tb;
}
private Query.Form.QueryTextBox.CustomTextBox tb;
}
}

View File

@ -8,84 +8,84 @@ namespace Query.Form;
sealed partial class QueryTextBox : UserControl {
public event EventHandler<CommandEventArgs>? CommandRan;
private CommandHistory history = null!;
private Action<string> log = null!;
public QueryTextBox() {
InitializeComponent();
}
public void Setup(CommandHistory historyObj, Action<string> logFunc) {
history = historyObj;
log = logFunc;
}
private void OnCommandRan() {
CommandRan?.Invoke(this, new CommandEventArgs(tb.Text));
}
private sealed class CustomTextBox : TextBox {
private string lastInputStr = string.Empty;
private int lastInputPos = 0;
private bool doResetHistoryMemory;
private bool lastArrowShift;
private int historyOffset;
public CustomTextBox() {
TextChanged += CustomTextBox_TextChanged;
}
protected override void OnKeyDown(KeyEventArgs e) {
QueryTextBox input = (QueryTextBox) Parent!;
CommandHistory history = input.history;
Keys key = e.KeyCode;
bool handled = false;
switch (key) {
case Keys.Enter:
if (Text != string.Empty) {
input.OnCommandRan();
Text = string.Empty;
doResetHistoryMemory = true;
handled = true;
}
break;
case Keys.Up:
if (lastArrowShift != e.Shift) {
lastArrowShift = e.Shift;
historyOffset = 0;
}
--historyOffset;
if (InsertFromHistory(e.Shift ? history.Results : history.Queries)) {
++historyOffset;
}
handled = true;
break;
case Keys.Down:
if (lastArrowShift != e.Shift) {
lastArrowShift = e.Shift;
historyOffset = 0;
}
++historyOffset;
if (InsertFromHistory(e.Shift ? history.Results : history.Queries)) {
--historyOffset;
}
handled = true;
break;
case Keys.C:
if (e.Modifiers == Keys.Control) {
if (SelectionLength == 0 && history.Results.Count > 0) {
@ -94,47 +94,47 @@ sealed partial class QueryTextBox : UserControl {
handled = true;
}
}
break;
}
if (!handled && key != Keys.ControlKey && key != Keys.ShiftKey && key != Keys.Menu) {
doResetHistoryMemory = true;
}
e.Handled = e.SuppressKeyPress = handled;
base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyEventArgs e) {
base.OnKeyUp(e);
if (doResetHistoryMemory) {
doResetHistoryMemory = false;
ResetHistoryMemory();
}
}
private void CustomTextBox_TextChanged(object? sender, EventArgs e) {
ResetHistoryMemory();
}
// Management
private void ResetHistoryMemory() {
lastInputStr = Text;
lastInputPos = SelectionStart;
historyOffset = 0;
}
private bool InsertFromHistory(IList<string> collection) {
if (collection.Count == 0) {
return true;
}
int index = collection.Count + historyOffset;
bool wasClamped = false;
if (index < 0) {
index = 0;
wasClamped = true;
@ -143,13 +143,13 @@ sealed partial class QueryTextBox : UserControl {
index = collection.Count - 1;
wasClamped = true;
}
TextChanged -= CustomTextBox_TextChanged;
Text = lastInputStr.Insert(lastInputPos, collection[index]);
SelectionStart = lastInputPos + collection[index].Length;
SelectionLength = 0;
TextChanged += CustomTextBox_TextChanged;
return wasClamped;
}

View File

@ -8,6 +8,7 @@ static class Program {
[STAThread]
private static void Main() {
Application.EnableVisualStyles();
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}