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

Compare commits

...

3 Commits

Author SHA1 Message Date
d52ec8a615
Release 2.1.0 2025-07-03 07:47:25 +02:00
5d640de7e5
Preserve rational number arithmetic for roots of perfect powers 2025-07-03 06:35:39 +02:00
d69c4672d6
Reformat code 2025-07-03 06:22:09 +02:00
22 changed files with 272 additions and 258 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) {
Fraction fractionExponent = Fraction.ReduceToProperFraction(rationalExponent.GetImproperFraction());
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

@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>chylex</Authors>
<Version>2.0.0.0</Version>
<Version>2.1.0.0</Version>
</PropertyGroup>
<PropertyGroup>

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

@ -157,4 +157,4 @@
AEE=
</value>
</data>
</root>
</root>

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

@ -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;
}