mirror of
https://github.com/chylex/Query.git
synced 2025-09-17 18:24:47 +02:00
Compare commits
8 Commits
2b9d5bba8f
...
main
Author | SHA1 | Date | |
---|---|---|---|
9a280e0d58
|
|||
6caf8c7eb2
|
|||
1c92d67fa3
|
|||
963f8da60c
|
|||
b40fb751d0
|
|||
d52ec8a615
|
|||
5d640de7e5
|
|||
d69c4672d6
|
@@ -1,40 +1,41 @@
|
|||||||
using Calculator.Math;
|
using System.Collections.Immutable;
|
||||||
|
using Calculator.Math;
|
||||||
using Calculator.Parser;
|
using Calculator.Parser;
|
||||||
|
|
||||||
namespace Calculator;
|
namespace Calculator;
|
||||||
|
|
||||||
public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUnit> {
|
public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUnit> {
|
||||||
public NumberWithUnit VisitNumber(Expression.Number number) {
|
public NumberWithUnit VisitNumber(Expression.Number number) {
|
||||||
return new NumberWithUnit(number.NumberToken.Value, null);
|
return number.NumberToken.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NumberWithUnit VisitNumbersWithUnits(Expression.NumbersWithUnits numbersWithUnits) {
|
public NumberWithUnit VisitNumbersWithUnits(Expression.NumbersWithUnits numbersWithUnits) {
|
||||||
NumberWithUnit result = new Number.Rational(0);
|
NumberWithUnit result = new Number.Rational(0);
|
||||||
|
|
||||||
foreach ((Token.Number number, Unit unit) in numbersWithUnits.NumberTokensWithUnits) {
|
foreach ((Token.Number number, Unit unit) in numbersWithUnits.NumberTokensWithUnits) {
|
||||||
result += new NumberWithUnit(number.Value, unit);
|
result += new NumberWithUnit(number.Value, [ unit ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NumberWithUnit VisitGrouping(Expression.Grouping grouping) {
|
public NumberWithUnit VisitGrouping(Expression.Grouping grouping) {
|
||||||
return Evaluate(grouping.Expression);
|
return Evaluate(grouping.Expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
public NumberWithUnit VisitUnary(Expression.Unary unary) {
|
public NumberWithUnit VisitUnary(Expression.Unary unary) {
|
||||||
(Token.Simple op, Expression right) = unary;
|
(Token.Simple op, Expression right) = unary;
|
||||||
|
|
||||||
return op.Type switch {
|
return op.Type switch {
|
||||||
SimpleTokenType.PLUS => +Evaluate(right),
|
SimpleTokenType.PLUS => +Evaluate(right),
|
||||||
SimpleTokenType.MINUS => -Evaluate(right),
|
SimpleTokenType.MINUS => -Evaluate(right),
|
||||||
_ => throw new CalculatorException("Unsupported unary operator: " + op.Type)
|
_ => throw new CalculatorException("Unsupported unary operator: " + op.Type)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public NumberWithUnit VisitBinary(Expression.Binary binary) {
|
public NumberWithUnit VisitBinary(Expression.Binary binary) {
|
||||||
(Expression left, Token.Simple op, Expression right) = binary;
|
(Expression left, Token.Simple op, Expression right) = binary;
|
||||||
|
|
||||||
return op.Type switch {
|
return op.Type switch {
|
||||||
SimpleTokenType.PLUS => Evaluate(left) + Evaluate(right),
|
SimpleTokenType.PLUS => Evaluate(left) + Evaluate(right),
|
||||||
SimpleTokenType.MINUS => Evaluate(left) - Evaluate(right),
|
SimpleTokenType.MINUS => Evaluate(left) - Evaluate(right),
|
||||||
@@ -45,26 +46,26 @@ public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUn
|
|||||||
_ => throw new CalculatorException("Unsupported binary operator: " + op.Type)
|
_ => throw new CalculatorException("Unsupported binary operator: " + op.Type)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public NumberWithUnit VisitUnitAssignment(Expression.UnitAssignment unitAssignment) {
|
public NumberWithUnit VisitUnitAssignment(Expression.UnitAssignment unitAssignment) {
|
||||||
(Expression left, Unit right) = unitAssignment;
|
(Expression left, Unit right) = unitAssignment;
|
||||||
|
|
||||||
NumberWithUnit number = Evaluate(left);
|
NumberWithUnit number = Evaluate(left);
|
||||||
|
|
||||||
if (number.Unit is null) {
|
if (number.PrimaryUnit is null) {
|
||||||
return number with { Unit = right };
|
return new NumberWithUnit(number.Value, [ right ]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new CalculatorException("Expression already has a unit, cannot assign a new unit: " + right);
|
throw new CalculatorException("Expression already has a unit, cannot assign a new unit: " + right);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public NumberWithUnit VisitUnitConversion(Expression.UnitConversion unitConversion) {
|
public NumberWithUnit VisitUnitConversion(Expression.UnitConversion unitConversion) {
|
||||||
(Expression left, Unit unit) = unitConversion;
|
(Expression left, ImmutableArray<Unit> units) = unitConversion;
|
||||||
|
|
||||||
return Evaluate(left).ConvertTo(unit);
|
return Evaluate(left).ConvertTo(units);
|
||||||
}
|
}
|
||||||
|
|
||||||
private NumberWithUnit Evaluate(Expression expression) {
|
private NumberWithUnit Evaluate(Expression expression) {
|
||||||
return expression.Accept(this);
|
return expression.Accept(this);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using ExtendedNumerics;
|
using ExtendedNumerics;
|
||||||
|
|
||||||
@@ -16,65 +17,117 @@ public abstract record Number : IAdditionOperators<Number, Number, Number>,
|
|||||||
IUnaryPlusOperators<Number, Number>,
|
IUnaryPlusOperators<Number, Number>,
|
||||||
IUnaryNegationOperators<Number, Number>,
|
IUnaryNegationOperators<Number, Number>,
|
||||||
IAdditiveIdentity<Number, Number.Rational>,
|
IAdditiveIdentity<Number, Number.Rational>,
|
||||||
IMultiplicativeIdentity<Number, Number.Rational> {
|
IMultiplicativeIdentity<Number, Number.Rational>,
|
||||||
|
IComparable<Number> {
|
||||||
protected abstract decimal AsDecimal { get; }
|
protected abstract decimal AsDecimal { get; }
|
||||||
|
|
||||||
|
public abstract Number WholePart { get; }
|
||||||
|
public abstract bool IsZero { get; }
|
||||||
|
|
||||||
public abstract Number Pow(Number exponent);
|
public abstract Number Pow(Number exponent);
|
||||||
|
|
||||||
public abstract string ToString(IFormatProvider? formatProvider);
|
public abstract string ToString(IFormatProvider? formatProvider);
|
||||||
|
|
||||||
|
public virtual int CompareTo(Number? other) {
|
||||||
|
return AsDecimal.CompareTo(other?.AsDecimal);
|
||||||
|
}
|
||||||
|
|
||||||
public sealed override string ToString() {
|
public sealed override string ToString() {
|
||||||
return ToString(CultureInfo.InvariantCulture);
|
return ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an integer number with arbitrary precision.
|
/// Represents an integer number with arbitrary precision.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record Rational(BigRational Value) : Number {
|
public sealed record Rational(BigRational Value) : Number {
|
||||||
protected override decimal AsDecimal => (decimal) Value;
|
protected override decimal AsDecimal => (decimal) Value;
|
||||||
|
|
||||||
|
public override Rational WholePart => new (Value.WholePart);
|
||||||
|
public override bool IsZero => Value.IsZero;
|
||||||
|
|
||||||
public override Number Pow(Number exponent) {
|
public override Number Pow(Number exponent) {
|
||||||
if (exponent is Rational { Value: {} rationalExponent }) {
|
if (exponent is Rational { Value: {} rationalExponent }) {
|
||||||
Fraction fractionExponent = rationalExponent.GetImproperFraction();
|
Fraction fractionExponent = Fraction.ReduceToProperFraction(rationalExponent.GetImproperFraction());
|
||||||
if (fractionExponent.Denominator == 1 && fractionExponent.Numerator >= 0) {
|
|
||||||
|
if (fractionExponent.Numerator >= 0 && fractionExponent.Denominator == 1) {
|
||||||
try {
|
try {
|
||||||
return new Rational(BigRational.Pow(Value, fractionExponent.Numerator));
|
return new Rational(BigRational.Pow(Value, fractionExponent.Numerator));
|
||||||
} catch (OverflowException) {}
|
} 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) {
|
public override string ToString(IFormatProvider? formatProvider) {
|
||||||
Fraction fraction = Value.GetImproperFraction();
|
BigRational value = Fraction.ReduceToProperFraction(Value.GetImproperFraction());
|
||||||
return fraction.Denominator == 1 ? fraction.Numerator.ToString(formatProvider) : AsDecimal.ToString(formatProvider);
|
string wholePartStr = value.WholePart.ToString(formatProvider);
|
||||||
|
|
||||||
|
if (value.FractionalPart.IsZero) {
|
||||||
|
return wholePartStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
decimal fractionalPart = (decimal) value.FractionalPart;
|
||||||
|
|
||||||
|
if (fractionalPart == 0) {
|
||||||
|
return wholePartStr + ".0...";
|
||||||
|
}
|
||||||
|
|
||||||
|
string decimalPartStr = fractionalPart.ToString(formatProvider);
|
||||||
|
int decimalSeparatorIndex = decimalPartStr.TakeWhile(char.IsAsciiDigit).Count();
|
||||||
|
|
||||||
|
return wholePartStr + decimalPartStr[decimalSeparatorIndex..];
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int CompareTo(Number? other) {
|
||||||
|
if (other is Rational rational) {
|
||||||
|
return Value.CompareTo(rational.Value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return base.CompareTo(other);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a decimal number with limited precision.
|
/// Represents a decimal number with limited precision.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record Decimal(decimal Value) : Number {
|
public sealed record Decimal(decimal Value) : Number {
|
||||||
public Decimal(double value) : this((decimal) value) {}
|
public Decimal(double value) : this((decimal) value) {}
|
||||||
|
|
||||||
protected override decimal AsDecimal => Value;
|
protected override decimal AsDecimal => Value;
|
||||||
|
|
||||||
|
public override Decimal WholePart => new (decimal.Floor(Value));
|
||||||
|
public override bool IsZero => Value == 0;
|
||||||
|
|
||||||
public override Number Pow(Number exponent) {
|
public override Number Pow(Number exponent) {
|
||||||
double doubleValue = (double) Value;
|
double doubleValue = (double) Value;
|
||||||
double doubleExponent = (double) exponent.AsDecimal;
|
double doubleExponent = (double) exponent.AsDecimal;
|
||||||
return new Decimal(System.Math.Pow(doubleValue, doubleExponent));
|
return new Decimal(System.Math.Pow(doubleValue, doubleExponent));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString(IFormatProvider? formatProvider) {
|
public override string ToString(IFormatProvider? formatProvider) {
|
||||||
return Value.ToString(formatProvider);
|
return Value.ToString(formatProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static implicit operator Number(BigRational value) {
|
public static implicit operator Number(BigRational value) {
|
||||||
return new Rational(value);
|
return new Rational(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static implicit operator Number(int value) {
|
public static implicit operator Number(int value) {
|
||||||
return new Rational(value);
|
return new Rational(value);
|
||||||
}
|
}
|
||||||
@@ -85,65 +138,65 @@ public abstract record Number : IAdditionOperators<Number, Number, Number>,
|
|||||||
|
|
||||||
public static Rational AdditiveIdentity => new (BigInteger.Zero);
|
public static Rational AdditiveIdentity => new (BigInteger.Zero);
|
||||||
public static Rational MultiplicativeIdentity => new (BigInteger.One);
|
public static Rational MultiplicativeIdentity => new (BigInteger.One);
|
||||||
|
|
||||||
public static Number Add(Number left, Number right) {
|
public static Number Add(Number left, Number right) {
|
||||||
return left + right;
|
return left + right;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number Subtract(Number left, Number right) {
|
public static Number Subtract(Number left, Number right) {
|
||||||
return left - right;
|
return left - right;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number Multiply(Number left, Number right) {
|
public static Number Multiply(Number left, Number right) {
|
||||||
return left * right;
|
return left * right;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number Divide(Number left, Number right) {
|
public static Number Divide(Number left, Number right) {
|
||||||
return left / right;
|
return left / right;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number Remainder(Number left, Number right) {
|
public static Number Remainder(Number left, Number right) {
|
||||||
return left % right;
|
return left % right;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number Pow(Number left, Number right) {
|
public static Number Pow(Number left, Number right) {
|
||||||
return left.Pow(right);
|
return left.Pow(right);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number operator +(Number value) {
|
public static Number operator +(Number value) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number operator -(Number value) {
|
public static Number operator -(Number value) {
|
||||||
return Operate(value, BigRational.Negate, decimal.Negate);
|
return Operate(value, BigRational.Negate, decimal.Negate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number operator +(Number left, Number right) {
|
public static Number operator +(Number left, Number right) {
|
||||||
return Operate(left, right, BigRational.Add, decimal.Add);
|
return Operate(left, right, BigRational.Add, decimal.Add);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number operator -(Number left, Number right) {
|
public static Number operator -(Number left, Number right) {
|
||||||
return Operate(left, right, BigRational.Subtract, decimal.Subtract);
|
return Operate(left, right, BigRational.Subtract, decimal.Subtract);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number operator *(Number left, Number right) {
|
public static Number operator *(Number left, Number right) {
|
||||||
return Operate(left, right, BigRational.Multiply, decimal.Multiply);
|
return Operate(left, right, BigRational.Multiply, decimal.Multiply);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number operator /(Number left, Number right) {
|
public static Number operator /(Number left, Number right) {
|
||||||
return Operate(left, right, BigRational.Divide, decimal.Divide);
|
return Operate(left, right, BigRational.Divide, decimal.Divide);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Number operator %(Number left, Number right) {
|
public static Number operator %(Number left, Number right) {
|
||||||
return Operate(left, right, BigRational.Mod, decimal.Remainder);
|
return Operate(left, right, BigRational.Mod, decimal.Remainder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Number Operate(Number value, Func<BigRational, BigRational> rationalOperation, Func<decimal, decimal> decimalOperation) {
|
private static Number Operate(Number value, Func<BigRational, BigRational> rationalOperation, Func<decimal, decimal> decimalOperation) {
|
||||||
return value is Rational rational
|
return value is Rational rational
|
||||||
? new Rational(rationalOperation(rational.Value))
|
? new Rational(rationalOperation(rational.Value))
|
||||||
: new Decimal(decimalOperation(value.AsDecimal));
|
: new Decimal(decimalOperation(value.AsDecimal));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Number Operate(Number left, Number right, Func<BigRational, BigRational, BigRational> rationalOperation, Func<decimal, decimal, decimal> decimalOperation) {
|
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
|
return left is Rational leftRational && right is Rational rightRational
|
||||||
? new Rational(rationalOperation(leftRational.Value, rightRational.Value))
|
? new Rational(rationalOperation(leftRational.Value, rightRational.Value))
|
||||||
|
@@ -1,57 +1,109 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Calculator.Math;
|
namespace Calculator.Math;
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||||
public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAdditionOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
public sealed class NumberWithUnit : IAdditionOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||||
ISubtractionOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
ISubtractionOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||||
IMultiplyOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
IMultiplyOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||||
IDivisionOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
IDivisionOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||||
IModulusOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
IModulusOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||||
IUnaryPlusOperators<NumberWithUnit, NumberWithUnit>,
|
IUnaryPlusOperators<NumberWithUnit, NumberWithUnit>,
|
||||||
IUnaryNegationOperators<NumberWithUnit, NumberWithUnit> {
|
IUnaryNegationOperators<NumberWithUnit, NumberWithUnit> {
|
||||||
public NumberWithUnit ConvertTo(Unit targetUnit) {
|
public Number Value { get; }
|
||||||
if (Unit == null) {
|
public Unit? PrimaryUnit => Unit?.Primary;
|
||||||
return this with { Unit = targetUnit };
|
|
||||||
|
private UnitDescription? Unit { get; }
|
||||||
|
|
||||||
|
private NumberWithUnit(Number value, UnitDescription? unit) {
|
||||||
|
this.Value = value;
|
||||||
|
this.Unit = unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberWithUnit(Number value, ImmutableArray<Unit> units) : this(value, UnitDescription.From(units)) {}
|
||||||
|
|
||||||
|
private NumberWithUnit WithValue(Number value) {
|
||||||
|
return new NumberWithUnit(value, Unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberWithUnit ConvertTo(ImmutableArray<Unit> targetUnits) {
|
||||||
|
if (targetUnits.IsEmpty || Unit == null) {
|
||||||
|
return new NumberWithUnit(Value, targetUnits);
|
||||||
}
|
}
|
||||||
else if (Units.All.TryGetUniverse(Unit, out var universe) && universe.TryConvert(Number, Unit, targetUnit, out var converted)) {
|
else if (Unit.Universe.TryConvert(Value, Unit.Primary, targetUnits[0], out Number? converted)) {
|
||||||
return new NumberWithUnit(converted, targetUnit);
|
return new NumberWithUnit(converted, targetUnits);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new ArithmeticException("Cannot convert '" + Unit + "' to '" + targetUnit + "'");
|
throw new ArithmeticException("Cannot convert '" + Unit.Primary + "' to '" + targetUnits[0] + "'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ToString(IFormatProvider? formatProvider) {
|
public string ToString(IFormatProvider? formatProvider) {
|
||||||
string number = Number.ToString(formatProvider);
|
if (Unit == null) {
|
||||||
return Unit == null ? number : number + " " + Unit;
|
return Value.ToString(formatProvider);
|
||||||
|
}
|
||||||
|
else if (Unit.Display.Length <= 1) {
|
||||||
|
return Value.ToString(formatProvider) + " " + Unit.Primary;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ImmutableArray<Unit> unitsFromLargest = [..Unit.Display.OrderByDescending(unit => Unit.Universe.Convert(value: 1, fromUnit: unit, toUnit: Unit.Primary))];
|
||||||
|
Unit smallestUnit = unitsFromLargest[^1];
|
||||||
|
Number remaining = Unit.Universe.Convert(Value, Unit.Primary, smallestUnit);
|
||||||
|
|
||||||
|
StringBuilder formatted = new StringBuilder();
|
||||||
|
|
||||||
|
void AppendNumberWithUnit(Number value, Unit unit) {
|
||||||
|
formatted.Append(value.ToString(formatProvider));
|
||||||
|
formatted.Append(' ');
|
||||||
|
formatted.Append(unit);
|
||||||
|
formatted.Append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Unit nextUnit in unitsFromLargest[..^1]) {
|
||||||
|
Number wholePart = Unit.Universe.Convert(remaining, smallestUnit, nextUnit).WholePart;
|
||||||
|
|
||||||
|
if (!wholePart.IsZero) {
|
||||||
|
AppendNumberWithUnit(wholePart, nextUnit);
|
||||||
|
remaining -= Unit.Universe.Convert(wholePart, nextUnit, smallestUnit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!remaining.IsZero || formatted.Length == 0) {
|
||||||
|
AppendNumberWithUnit(remaining, smallestUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted.Remove(formatted.Length - 1, length: 1).ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() {
|
public override string ToString() {
|
||||||
return ToString(CultureInfo.InvariantCulture);
|
return ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static implicit operator NumberWithUnit(Number number) {
|
public static implicit operator NumberWithUnit(Number number) {
|
||||||
return new NumberWithUnit(number, null);
|
return new NumberWithUnit(number, unit: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NumberWithUnit operator +(NumberWithUnit value) {
|
public static NumberWithUnit operator +(NumberWithUnit value) {
|
||||||
return value with { Number = +value.Number };
|
return value.WithValue(+value.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NumberWithUnit operator -(NumberWithUnit value) {
|
public static NumberWithUnit operator -(NumberWithUnit value) {
|
||||||
return value with { Number = -value.Number };
|
return value.WithValue(-value.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NumberWithUnit operator +(NumberWithUnit left, NumberWithUnit right) {
|
public static NumberWithUnit operator +(NumberWithUnit left, NumberWithUnit right) {
|
||||||
return Operate(left, right, Number.Add, static (leftNumber, leftUnit, rightNumber, rightUnit) => {
|
return Operate(left, right, Number.Add, static (leftNumber, leftUnit, rightNumber, rightUnit) => {
|
||||||
if (leftUnit == rightUnit) {
|
if (leftUnit.Primary == rightUnit.Primary) {
|
||||||
return new NumberWithUnit(leftNumber + rightNumber, leftUnit);
|
return new NumberWithUnit(leftNumber + rightNumber, leftUnit);
|
||||||
}
|
}
|
||||||
else if (Units.All.TryGetUniverse(leftUnit, out UnitUniverse? universe) && universe.TryConvert(rightNumber, rightUnit, leftUnit, out Number? rightConverted)) {
|
else if (rightUnit.Universe.TryConvert(rightNumber, fromUnit: rightUnit.Primary, toUnit: leftUnit.Primary, out Number? rightConverted)) {
|
||||||
return new NumberWithUnit(leftNumber + rightConverted, leftUnit);
|
return new NumberWithUnit(leftNumber + rightConverted, leftUnit);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -59,13 +111,13 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NumberWithUnit operator -(NumberWithUnit left, NumberWithUnit right) {
|
public static NumberWithUnit operator -(NumberWithUnit left, NumberWithUnit right) {
|
||||||
return Operate(left, right, Number.Subtract, static (leftNumber, leftUnit, rightNumber, rightUnit) => {
|
return Operate(left, right, Number.Subtract, static (leftNumber, leftUnit, rightNumber, rightUnit) => {
|
||||||
if (leftUnit == rightUnit) {
|
if (leftUnit.Primary == rightUnit.Primary) {
|
||||||
return new NumberWithUnit(leftNumber - rightNumber, leftUnit);
|
return new NumberWithUnit(leftNumber - rightNumber, leftUnit);
|
||||||
}
|
}
|
||||||
else if (Units.All.TryGetUniverse(leftUnit, out UnitUniverse? universe) && universe.TryConvert(rightNumber, rightUnit, leftUnit, out Number? rightConverted)) {
|
else if (rightUnit.Universe.TryConvert(rightNumber, fromUnit: rightUnit.Primary, toUnit: leftUnit.Primary, out Number? rightConverted)) {
|
||||||
return new NumberWithUnit(leftNumber - rightConverted, leftUnit);
|
return new NumberWithUnit(leftNumber - rightConverted, leftUnit);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -73,36 +125,61 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NumberWithUnit operator *(NumberWithUnit left, NumberWithUnit right) {
|
public static NumberWithUnit operator *(NumberWithUnit left, NumberWithUnit right) {
|
||||||
return OperateWithoutUnits(left, right, Number.Multiply, "Cannot multiply");
|
return OperateWithoutUnits(left, right, Number.Multiply, "Cannot multiply");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NumberWithUnit operator /(NumberWithUnit left, NumberWithUnit right) {
|
public static NumberWithUnit operator /(NumberWithUnit left, NumberWithUnit right) {
|
||||||
return OperateWithoutUnits(left, right, Number.Divide, "Cannot divide");
|
return OperateWithoutUnits(left, right, Number.Divide, "Cannot divide");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static NumberWithUnit operator %(NumberWithUnit left, NumberWithUnit right) {
|
public static NumberWithUnit operator %(NumberWithUnit left, NumberWithUnit right) {
|
||||||
return OperateWithoutUnits(left, right, Number.Remainder, "Cannot modulo");
|
return OperateWithoutUnits(left, right, Number.Remainder, "Cannot modulo");
|
||||||
}
|
}
|
||||||
|
|
||||||
public NumberWithUnit Pow(NumberWithUnit exponent) {
|
public NumberWithUnit Pow(NumberWithUnit exponent) {
|
||||||
return OperateWithoutUnits(this, exponent, Number.Pow, "Cannot exponentiate");
|
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) {
|
private static NumberWithUnit Operate(NumberWithUnit left, NumberWithUnit right, Func<Number, Number, Number> withoutUnitsOperation, Func<Number, UnitDescription, Number, UnitDescription, NumberWithUnit> withUnitsOperation) {
|
||||||
if (right.Unit is null) {
|
if (right.Unit is null) {
|
||||||
return left with { Number = withoutUnitsOperation(left.Number, right.Number) };
|
return left.WithValue(withoutUnitsOperation(left.Value, right.Value));
|
||||||
}
|
}
|
||||||
else if (left.Unit is null) {
|
else if (left.Unit is null) {
|
||||||
return right with { Number = withoutUnitsOperation(left.Number, right.Number) };
|
return right.WithValue(withoutUnitsOperation(left.Value, right.Value));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return withUnitsOperation(left.Number, left.Unit, right.Number, right.Unit);
|
return withUnitsOperation(left.Value, left.Unit, right.Value, right.Unit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NumberWithUnit OperateWithoutUnits(NumberWithUnit left, NumberWithUnit right, Func<Number, Number, Number> withoutUnitsOperation, string messagePrefix) {
|
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 + "'"));
|
return Operate(left, right, withoutUnitsOperation, (_, leftUnit, _, rightUnit) => throw new ArithmeticException(messagePrefix + " '" + leftUnit + "' and '" + rightUnit + "'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record UnitDescription(Unit Primary, ImmutableArray<Unit> Display, UnitUniverse Universe) {
|
||||||
|
public static UnitDescription? From(ImmutableArray<Unit> units) {
|
||||||
|
if (units.IsEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Units.All.TryGetUniverse(units[0], out UnitUniverse? primaryUnitUniverse)) {
|
||||||
|
throw new ArgumentException("Unknown unit universe: " + units[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int index = 1; index < units.Length; index++) {
|
||||||
|
Unit secondaryUnit = units[index];
|
||||||
|
|
||||||
|
if (!Units.All.TryGetUniverse(secondaryUnit, out UnitUniverse? secondaryUnitUniverse)) {
|
||||||
|
throw new ArgumentException("Unknown unit universe: " + secondaryUnit);
|
||||||
|
}
|
||||||
|
else if (primaryUnitUniverse != secondaryUnitUniverse) {
|
||||||
|
throw new ArgumentException("All units must be from the same universe: " + secondaryUnitUniverse + " != " + primaryUnitUniverse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UnitDescription(units[0], units, primaryUnitUniverse);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,11 +10,16 @@ using ExtendedNumerics;
|
|||||||
namespace Calculator.Math;
|
namespace Calculator.Math;
|
||||||
|
|
||||||
sealed class UnitUniverse(
|
sealed class UnitUniverse(
|
||||||
|
string name,
|
||||||
FrozenDictionary<Unit, Func<Number, Number>> unitToConversionToPrimaryUnit,
|
FrozenDictionary<Unit, Func<Number, Number>> unitToConversionToPrimaryUnit,
|
||||||
FrozenDictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit
|
FrozenDictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit
|
||||||
) {
|
) {
|
||||||
public ImmutableArray<Unit> AllUnits => unitToConversionToPrimaryUnit.Keys;
|
public ImmutableArray<Unit> AllUnits => unitToConversionToPrimaryUnit.Keys;
|
||||||
|
|
||||||
|
internal Number Convert(Number value, Unit fromUnit, Unit toUnit) {
|
||||||
|
return TryConvert(value, fromUnit, toUnit, out var converted) ? converted : throw new ArgumentException("Cannot convert from " + fromUnit + " to " + toUnit);
|
||||||
|
}
|
||||||
|
|
||||||
internal bool TryConvert(Number value, Unit fromUnit, Unit toUnit, [NotNullWhen(true)] out Number? converted) {
|
internal bool TryConvert(Number value, Unit fromUnit, Unit toUnit, [NotNullWhen(true)] out Number? converted) {
|
||||||
if (fromUnit == toUnit) {
|
if (fromUnit == toUnit) {
|
||||||
converted = value;
|
converted = value;
|
||||||
@@ -29,89 +34,96 @@ sealed class UnitUniverse(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override string ToString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed record SI(string ShortPrefix, string LongPrefix, int Factor) {
|
internal sealed record SI(string ShortPrefix, string LongPrefix, int Factor) {
|
||||||
internal static readonly List<SI> All = [
|
internal static readonly List<SI> All = [
|
||||||
new SI("Q", "quetta", 30),
|
new ("Q", "quetta", Factor: 30),
|
||||||
new SI("R", "ronna", 27),
|
new ("R", "ronna", Factor: 27),
|
||||||
new SI("Y", "yotta", 24),
|
new ("Y", "yotta", Factor: 24),
|
||||||
new SI("Z", "zetta", 21),
|
new ("Z", "zetta", Factor: 21),
|
||||||
new SI("E", "exa", 18),
|
new ("E", "exa", Factor: 18),
|
||||||
new SI("P", "peta", 15),
|
new ("P", "peta", Factor: 15),
|
||||||
new SI("T", "tera", 12),
|
new ("T", "tera", Factor: 12),
|
||||||
new SI("G", "giga", 9),
|
new ("G", "giga", Factor: 9),
|
||||||
new SI("M", "mega", 6),
|
new ("M", "mega", Factor: 6),
|
||||||
new SI("k", "kilo", 3),
|
new ("k", "kilo", Factor: 3),
|
||||||
new SI("h", "hecto", 2),
|
new ("h", "hecto", Factor: 2),
|
||||||
new SI("da", "deca", 1),
|
new ("da", "deca", Factor: 1),
|
||||||
new SI("d", "deci", -1),
|
new ("d", "deci", Factor: -1),
|
||||||
new SI("c", "centi", -2),
|
new ("c", "centi", Factor: -2),
|
||||||
new SI("m", "milli", -3),
|
new ("m", "milli", Factor: -3),
|
||||||
new SI("μ", "micro", -6),
|
new ("μ", "micro", Factor: -6),
|
||||||
new SI("n", "nano", -9),
|
new ("n", "nano", Factor: -9),
|
||||||
new SI("p", "pico", -12),
|
new ("p", "pico", Factor: -12),
|
||||||
new SI("f", "femto", -15),
|
new ("f", "femto", Factor: -15),
|
||||||
new SI("a", "atto", -18),
|
new ("a", "atto", Factor: -18),
|
||||||
new SI("z", "zepto", -21),
|
new ("z", "zepto", Factor: -21),
|
||||||
new SI("y", "yocto", -24),
|
new ("y", "yocto", Factor: -24),
|
||||||
new SI("r", "ronto", -27),
|
new ("r", "ronto", Factor: -27),
|
||||||
new SI("q", "quecto", -30)
|
new ("q", "quecto", Factor: -30),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class Builder {
|
internal sealed class Builder {
|
||||||
|
private readonly string name;
|
||||||
private readonly Unit primaryUnit;
|
private readonly Unit primaryUnit;
|
||||||
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionToPrimaryUnit = new (ReferenceEqualityComparer.Instance);
|
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionToPrimaryUnit = new (ReferenceEqualityComparer.Instance);
|
||||||
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit = new (ReferenceEqualityComparer.Instance);
|
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit = new (ReferenceEqualityComparer.Instance);
|
||||||
|
|
||||||
public Builder(Unit primaryUnit) {
|
public Builder(string name, Unit primaryUnit) {
|
||||||
|
this.name = name;
|
||||||
this.primaryUnit = primaryUnit;
|
this.primaryUnit = primaryUnit;
|
||||||
AddUnit(primaryUnit, 1);
|
AddUnit(primaryUnit, amountInPrimaryUnit: 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder AddUnit(Unit unit, Func<Number, Number> convertToPrimaryUnit, Func<Number, Number> convertFromPrimaryUnit) {
|
public Builder AddUnit(Unit unit, Func<Number, Number> convertToPrimaryUnit, Func<Number, Number> convertFromPrimaryUnit) {
|
||||||
unitToConversionToPrimaryUnit.Add(unit, convertToPrimaryUnit);
|
unitToConversionToPrimaryUnit.Add(unit, convertToPrimaryUnit);
|
||||||
unitToConversionFromPrimaryUnit.Add(unit, convertFromPrimaryUnit);
|
unitToConversionFromPrimaryUnit.Add(unit, convertFromPrimaryUnit);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder AddUnit(Unit unit, Number amountInPrimaryUnit) {
|
public Builder AddUnit(Unit unit, Number amountInPrimaryUnit) {
|
||||||
return AddUnit(unit, number => number * amountInPrimaryUnit, number => number / amountInPrimaryUnit);
|
return AddUnit(unit, number => number * amountInPrimaryUnit, number => number / amountInPrimaryUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddUnitSI(SI si, Func<SI, Unit> unitFactory, Func<int, int> factorModifier) {
|
private void AddUnitSI(SI si, Func<SI, Unit> unitFactory, Func<int, int> factorModifier) {
|
||||||
int factor = factorModifier(si.Factor);
|
int factor = factorModifier(si.Factor);
|
||||||
BigInteger powerOfTen = BigInteger.Pow(10, System.Math.Abs(factor));
|
BigInteger powerOfTen = BigInteger.Pow(value: 10, System.Math.Abs(factor));
|
||||||
BigRational amountInPrimaryUnit = factor > 0 ? new BigRational(powerOfTen) : new BigRational(1, powerOfTen);
|
BigRational amountInPrimaryUnit = factor > 0 ? new BigRational(powerOfTen) : new BigRational(numerator: 1, powerOfTen);
|
||||||
AddUnit(unitFactory(si), amountInPrimaryUnit);
|
AddUnit(unitFactory(si), amountInPrimaryUnit);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder AddSI(Func<SI, Unit> unitFactory, Func<int, int> factorModifier) {
|
public Builder AddSI(Func<SI, Unit> unitFactory, Func<int, int> factorModifier) {
|
||||||
foreach (SI si in SI.All) {
|
foreach (SI si in SI.All) {
|
||||||
AddUnitSI(si, unitFactory, factorModifier);
|
AddUnitSI(si, unitFactory, factorModifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder AddSI(Func<int, int> factorModifier) {
|
public Builder AddSI(Func<int, int> factorModifier) {
|
||||||
Unit PrefixPrimaryUnit(SI si) {
|
Unit PrefixPrimaryUnit(SI si) {
|
||||||
return new Unit(si.ShortPrefix + primaryUnit.ShortName, [..primaryUnit.LongNames.Select(longName => si.LongPrefix + longName)]);
|
return new Unit(si.ShortPrefix + primaryUnit.ShortName, [..primaryUnit.LongNames.Select(longName => si.LongPrefix + longName)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (SI si in SI.All) {
|
foreach (SI si in SI.All) {
|
||||||
AddUnitSI(si, PrefixPrimaryUnit, factorModifier);
|
AddUnitSI(si, PrefixPrimaryUnit, factorModifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder AddSI() {
|
public Builder AddSI() {
|
||||||
return AddSI(static factor => factor);
|
return AddSI(static factor => factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UnitUniverse Build() {
|
public UnitUniverse Build() {
|
||||||
return new UnitUniverse(
|
return new UnitUniverse(
|
||||||
|
name,
|
||||||
unitToConversionToPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance),
|
unitToConversionToPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance),
|
||||||
unitToConversionFromPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance)
|
unitToConversionFromPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance)
|
||||||
);
|
);
|
||||||
|
@@ -9,7 +9,7 @@ sealed class UnitUniverses {
|
|||||||
private readonly FrozenDictionary<Unit, UnitUniverse> unitToUniverse;
|
private readonly FrozenDictionary<Unit, UnitUniverse> unitToUniverse;
|
||||||
|
|
||||||
public WordLookupTrieNode UnitLookupByWords { get; }
|
public WordLookupTrieNode UnitLookupByWords { get; }
|
||||||
|
|
||||||
internal UnitUniverses(params UnitUniverse[] universes) {
|
internal UnitUniverses(params UnitUniverse[] universes) {
|
||||||
Dictionary<Unit, UnitUniverse> unitToUniverseBuilder = new (ReferenceEqualityComparer.Instance);
|
Dictionary<Unit, UnitUniverse> unitToUniverseBuilder = new (ReferenceEqualityComparer.Instance);
|
||||||
WordLookupTrieNode.Builder unitLookupByWordsBuilder = new ();
|
WordLookupTrieNode.Builder unitLookupByWordsBuilder = new ();
|
||||||
@@ -20,7 +20,7 @@ sealed class UnitUniverses {
|
|||||||
unitLookupByWordsBuilder.Add(unit);
|
unitLookupByWordsBuilder.Add(unit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unitToUniverse = unitToUniverseBuilder.ToFrozenDictionary(ReferenceEqualityComparer.Instance);
|
unitToUniverse = unitToUniverseBuilder.ToFrozenDictionary(ReferenceEqualityComparer.Instance);
|
||||||
UnitLookupByWords = unitLookupByWordsBuilder.Build();
|
UnitLookupByWords = unitLookupByWordsBuilder.Build();
|
||||||
}
|
}
|
||||||
@@ -58,14 +58,14 @@ sealed class UnitUniverses {
|
|||||||
private void Add(string name, Unit unit) {
|
private void Add(string name, Unit unit) {
|
||||||
Node node = root;
|
Node node = root;
|
||||||
string[] words = name.Split(' ');
|
string[] words = name.Split(' ');
|
||||||
|
|
||||||
foreach (string word in words.AsSpan(..^1)) {
|
foreach (string word in words.AsSpan(..^1)) {
|
||||||
node = node.Child(word);
|
node = node.Child(word);
|
||||||
}
|
}
|
||||||
|
|
||||||
node.Children.Add(words[^1], Node.Create(unit));
|
node.Children.Add(words[^1], Node.Create(unit));
|
||||||
}
|
}
|
||||||
|
|
||||||
public WordLookupTrieNode Build() {
|
public WordLookupTrieNode Build() {
|
||||||
return Build(root);
|
return Build(root);
|
||||||
}
|
}
|
||||||
|
@@ -17,22 +17,22 @@ static class Units {
|
|||||||
public static UnitUniverse Angle { get; } = AngleUniverse().Build();
|
public static UnitUniverse Angle { get; } = AngleUniverse().Build();
|
||||||
public static UnitUniverse Temperature { get; } = TemperatureUniverse().Build();
|
public static UnitUniverse Temperature { get; } = TemperatureUniverse().Build();
|
||||||
public static UnitUniverse InformationEntropy { get; } = InformationEntropyUniverse().Build();
|
public static UnitUniverse InformationEntropy { get; } = InformationEntropyUniverse().Build();
|
||||||
|
|
||||||
public static UnitUniverses All { get; } = new (Time, Length, Mass, Area, Volume, Angle, Temperature, InformationEntropy);
|
public static UnitUniverses All { get; } = new (Time, Length, Mass, Area, Volume, Angle, Temperature, InformationEntropy);
|
||||||
|
|
||||||
private static UnitUniverse.Builder TimeUniverse() {
|
private static UnitUniverse.Builder TimeUniverse() {
|
||||||
var minute = 60;
|
var minute = 60;
|
||||||
var hour = minute * 60;
|
var hour = minute * 60;
|
||||||
var day = hour * 24;
|
var day = hour * 24;
|
||||||
var week = day * 7;
|
var week = day * 7;
|
||||||
|
|
||||||
return new UnitUniverse.Builder(new Unit("s", Pluralize("second")))
|
return new UnitUniverse.Builder("Time", new Unit("s", Pluralize("second")))
|
||||||
.AddUnit(new Unit("min", Pluralize("minute")), minute)
|
.AddUnit(new Unit("min", Pluralize("minute")), minute)
|
||||||
.AddUnit(new Unit("h", Pluralize("hour")), hour)
|
.AddUnit(new Unit("h", Pluralize("hour")), hour)
|
||||||
.AddUnit(new Unit("d", Pluralize("day")), day)
|
.AddUnit(new Unit("d", Pluralize("day")), day)
|
||||||
.AddUnit(new Unit("wk", Pluralize("week")), week);
|
.AddUnit(new Unit("wk", Pluralize("week")), week);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UnitUniverse.Builder LengthUniverse() {
|
private static UnitUniverse.Builder LengthUniverse() {
|
||||||
var inch = Parse("0", "0254");
|
var inch = Parse("0", "0254");
|
||||||
var foot = inch * 12;
|
var foot = inch * 12;
|
||||||
@@ -41,8 +41,8 @@ static class Units {
|
|||||||
var mile = yard * 1760;
|
var mile = yard * 1760;
|
||||||
var nauticalMile = 1_852;
|
var nauticalMile = 1_852;
|
||||||
var lightYear = 9_460_730_472_580_800;
|
var lightYear = 9_460_730_472_580_800;
|
||||||
|
|
||||||
return new UnitUniverse.Builder(new Unit("m", Pluralize("meter", "metre")))
|
return new UnitUniverse.Builder("Length", new Unit("m", Pluralize("meter", "metre")))
|
||||||
.AddSI()
|
.AddSI()
|
||||||
.AddUnit(new Unit("in", [ "inch", "inches", "\"" ]), inch)
|
.AddUnit(new Unit("in", [ "inch", "inches", "\"" ]), inch)
|
||||||
.AddUnit(new Unit("ft", [ "foot", "feet", "'" ]), foot)
|
.AddUnit(new Unit("ft", [ "foot", "feet", "'" ]), foot)
|
||||||
@@ -52,21 +52,21 @@ static class Units {
|
|||||||
.AddUnit(new Unit("nmi", Pluralize("nautical mile")), nauticalMile)
|
.AddUnit(new Unit("nmi", Pluralize("nautical mile")), nauticalMile)
|
||||||
.AddUnit(new Unit("ly", Pluralize("light-year", "light year")), lightYear);
|
.AddUnit(new Unit("ly", Pluralize("light-year", "light year")), lightYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UnitUniverse.Builder MassUniverse() {
|
private static UnitUniverse.Builder MassUniverse() {
|
||||||
var pound = Parse("453", "59237");
|
var pound = Parse("453", "59237");
|
||||||
var stone = pound * 14;
|
var stone = pound * 14;
|
||||||
var ounce = pound / 16;
|
var ounce = pound / 16;
|
||||||
var dram = ounce / 16;
|
var dram = ounce / 16;
|
||||||
|
|
||||||
return new UnitUniverse.Builder(new Unit("g", Pluralize("gram")))
|
return new UnitUniverse.Builder("Mass", new Unit("g", Pluralize("gram")))
|
||||||
.AddSI()
|
.AddSI()
|
||||||
.AddUnit(new Unit("lb", [ "lbs", "pound", "pounds" ]), pound)
|
.AddUnit(new Unit("lb", [ "lbs", "pound", "pounds" ]), pound)
|
||||||
.AddUnit(new Unit("st", Pluralize("stone")), stone)
|
.AddUnit(new Unit("st", Pluralize("stone")), stone)
|
||||||
.AddUnit(new Unit("oz", Pluralize("ounce")), ounce)
|
.AddUnit(new Unit("oz", Pluralize("ounce")), ounce)
|
||||||
.AddUnit(new Unit("dr", Pluralize("dram")), dram);
|
.AddUnit(new Unit("dr", Pluralize("dram")), dram);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UnitUniverse.Builder AreaUniverse() {
|
private static UnitUniverse.Builder AreaUniverse() {
|
||||||
static Unit SquareMeter(string shortPrefix, string longPrefix) {
|
static Unit SquareMeter(string shortPrefix, string longPrefix) {
|
||||||
return new Unit(shortPrefix + "m2", [
|
return new Unit(shortPrefix + "m2", [
|
||||||
@@ -80,13 +80,13 @@ static class Units {
|
|||||||
..Pluralize($"square {longPrefix}meter", $"square {longPrefix}metre")
|
..Pluralize($"square {longPrefix}meter", $"square {longPrefix}metre")
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new UnitUniverse.Builder(SquareMeter(string.Empty, string.Empty))
|
return new UnitUniverse.Builder("Area", SquareMeter(string.Empty, string.Empty))
|
||||||
.AddSI(static si => SquareMeter(si.ShortPrefix, si.LongPrefix), static factor => factor * 2)
|
.AddSI(static si => SquareMeter(si.ShortPrefix, si.LongPrefix), static factor => factor * 2)
|
||||||
.AddUnit(new Unit("a", Pluralize("are")), 100)
|
.AddUnit(new Unit("a", Pluralize("are")), amountInPrimaryUnit: 100)
|
||||||
.AddUnit(new Unit("ha", Pluralize("hectare")), 10_000);
|
.AddUnit(new Unit("ha", Pluralize("hectare")), amountInPrimaryUnit: 10_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UnitUniverse.Builder VolumeUniverse() {
|
private static UnitUniverse.Builder VolumeUniverse() {
|
||||||
static Unit CubicMeter(string shortPrefix, string longPrefix) {
|
static Unit CubicMeter(string shortPrefix, string longPrefix) {
|
||||||
return new Unit(shortPrefix + "m3", [
|
return new Unit(shortPrefix + "m3", [
|
||||||
@@ -99,57 +99,57 @@ static class Units {
|
|||||||
..Pluralize($"cubic {longPrefix}meter", $"cubic {longPrefix}metre")
|
..Pluralize($"cubic {longPrefix}meter", $"cubic {longPrefix}metre")
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new UnitUniverse.Builder(new Unit("l", Pluralize("litre", "liter")))
|
return new UnitUniverse.Builder("Volume", new Unit("l", Pluralize("litre", "liter")))
|
||||||
.AddSI()
|
.AddSI()
|
||||||
.AddUnit(CubicMeter(string.Empty, string.Empty), 1000)
|
.AddUnit(CubicMeter(string.Empty, string.Empty), amountInPrimaryUnit: 1000)
|
||||||
.AddSI(static si => CubicMeter(si.ShortPrefix, si.LongPrefix), static factor => (factor * 3) + 3);
|
.AddSI(static si => CubicMeter(si.ShortPrefix, si.LongPrefix), static factor => (factor * 3) + 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UnitUniverse.Builder AngleUniverse() {
|
private static UnitUniverse.Builder AngleUniverse() {
|
||||||
return new UnitUniverse.Builder(new Unit("deg", [ "°", "degree", "degrees" ]))
|
return new UnitUniverse.Builder("Angle", new Unit("deg", [ "°", "degree", "degrees" ]))
|
||||||
.AddUnit(new Unit("rad", Pluralize("radian")), new Number.Decimal((decimal) System.Math.PI / 180M))
|
.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));
|
.AddUnit(new Unit("grad", Pluralize("gradian", "grade", "gon")), Ratio(numerator: 9, denominator: 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BigRational KelvinOffset { get; } = Parse("273", "15");
|
private static BigRational KelvinOffset { get; } = Parse("273", "15");
|
||||||
|
|
||||||
private static UnitUniverse.Builder TemperatureUniverse() {
|
private static UnitUniverse.Builder TemperatureUniverse() {
|
||||||
return new UnitUniverse.Builder(new Unit("°C", [ "C", "Celsius", "celsius" ]))
|
return new UnitUniverse.Builder("Temperature", 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("°F", [ "F", "Fahrenheit", "fahrenheit" ]), static f => (f - 32) * Ratio(numerator: 5, denominator: 9), static c => c * Ratio(numerator: 9, denominator: 5) + 32)
|
||||||
.AddUnit(new Unit("K", [ "Kelvin", "kelvin" ]), static k => k - KelvinOffset, static c => c + KelvinOffset);
|
.AddUnit(new Unit("K", [ "Kelvin", "kelvin" ]), static k => k - KelvinOffset, static c => c + KelvinOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UnitUniverse.Builder InformationEntropyUniverse() {
|
private static UnitUniverse.Builder InformationEntropyUniverse() {
|
||||||
var bit = Ratio(1, 8);
|
var bit = Ratio(numerator: 1, denominator: 8);
|
||||||
var nibble = bit * 4;
|
var nibble = bit * 4;
|
||||||
|
|
||||||
return new UnitUniverse.Builder(new Unit("B", Pluralize("byte")))
|
return new UnitUniverse.Builder("Information Entropy", new Unit("B", Pluralize("byte")))
|
||||||
.AddSI()
|
.AddSI()
|
||||||
.AddUnit(new Unit("b", Pluralize("bit")), bit)
|
.AddUnit(new Unit("b", Pluralize("bit")), bit)
|
||||||
.AddUnit(new Unit("nibbles", [ "nibble" ]), nibble)
|
.AddUnit(new Unit("nibbles", [ "nibble" ]), nibble)
|
||||||
.AddUnit(new Unit("KiB", Pluralize("kibibyte")), Pow(1024, 1))
|
.AddUnit(new Unit("KiB", Pluralize("kibibyte")), Pow(value: 1024, exponent: 1))
|
||||||
.AddUnit(new Unit("MiB", Pluralize("mebibyte")), Pow(1024, 2))
|
.AddUnit(new Unit("MiB", Pluralize("mebibyte")), Pow(value: 1024, exponent: 2))
|
||||||
.AddUnit(new Unit("GiB", Pluralize("gibibyte")), Pow(1024, 3))
|
.AddUnit(new Unit("GiB", Pluralize("gibibyte")), Pow(value: 1024, exponent: 3))
|
||||||
.AddUnit(new Unit("TiB", Pluralize("tebibyte")), Pow(1024, 4))
|
.AddUnit(new Unit("TiB", Pluralize("tebibyte")), Pow(value: 1024, exponent: 4))
|
||||||
.AddUnit(new Unit("PiB", Pluralize("pebibyte")), Pow(1024, 5))
|
.AddUnit(new Unit("PiB", Pluralize("pebibyte")), Pow(value: 1024, exponent: 5))
|
||||||
.AddUnit(new Unit("EiB", Pluralize("exbibyte")), Pow(1024, 6))
|
.AddUnit(new Unit("EiB", Pluralize("exbibyte")), Pow(value: 1024, exponent: 6))
|
||||||
.AddUnit(new Unit("ZiB", Pluralize("zebibyte")), Pow(1024, 7))
|
.AddUnit(new Unit("ZiB", Pluralize("zebibyte")), Pow(value: 1024, exponent: 7))
|
||||||
.AddUnit(new Unit("YiB", Pluralize("yobibyte")), Pow(1024, 8));
|
.AddUnit(new Unit("YiB", Pluralize("yobibyte")), Pow(value: 1024, exponent: 8));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BigRational Parse(string integerPart, string fractionalPart) {
|
private static BigRational Parse(string integerPart, string fractionalPart) {
|
||||||
return Tokenizer.ParseNumber(integerPart, fractionalPart);
|
return Tokenizer.ParseNumber(integerPart, fractionalPart);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BigRational Ratio(long numerator, long denominator) {
|
private static BigRational Ratio(long numerator, long denominator) {
|
||||||
return new BigRational(numerator, denominator);
|
return new BigRational(numerator, denominator);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BigRational Pow(int value, int exponent) {
|
private static BigRational Pow(int value, int exponent) {
|
||||||
return BigRational.Pow(value, exponent);
|
return BigRational.Pow(value, exponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ImmutableArray<string> Pluralize(params string[] names) {
|
private static ImmutableArray<string> Pluralize(params string[] names) {
|
||||||
return [..names.SelectMany(static name => new [] { name, name + "s" })];
|
return [..names.SelectMany(static name => new [] { name, name + "s" })];
|
||||||
}
|
}
|
||||||
|
@@ -6,9 +6,9 @@ namespace Calculator.Parser;
|
|||||||
|
|
||||||
public abstract record Expression {
|
public abstract record Expression {
|
||||||
private Expression() {}
|
private Expression() {}
|
||||||
|
|
||||||
public abstract T Accept<T>(ExpressionVisitor<T> visitor);
|
public abstract T Accept<T>(ExpressionVisitor<T> visitor);
|
||||||
|
|
||||||
public sealed record Number(Token.Number NumberToken) : Expression {
|
public sealed record Number(Token.Number NumberToken) : Expression {
|
||||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||||
return visitor.VisitNumber(this);
|
return visitor.VisitNumber(this);
|
||||||
@@ -19,7 +19,7 @@ public abstract record Expression {
|
|||||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||||
return visitor.VisitNumbersWithUnits(this);
|
return visitor.VisitNumbersWithUnits(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() {
|
public override string ToString() {
|
||||||
return nameof(NumbersWithUnits) + " { " + string.Join(", ", NumberTokensWithUnits.Select(static (number, unit) => number + " " + unit)) + " }";
|
return nameof(NumbersWithUnits) + " { " + string.Join(", ", NumberTokensWithUnits.Select(static (number, unit) => number + " " + unit)) + " }";
|
||||||
}
|
}
|
||||||
@@ -30,26 +30,26 @@ public abstract record Expression {
|
|||||||
return visitor.VisitGrouping(this);
|
return visitor.VisitGrouping(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record Unary(Token.Simple Operator, Expression Right) : Expression {
|
public sealed record Unary(Token.Simple Operator, Expression Right) : Expression {
|
||||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||||
return visitor.VisitUnary(this);
|
return visitor.VisitUnary(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record Binary(Expression Left, Token.Simple Operator, Expression Right) : Expression {
|
public sealed record Binary(Expression Left, Token.Simple Operator, Expression Right) : Expression {
|
||||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||||
return visitor.VisitBinary(this);
|
return visitor.VisitBinary(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record UnitAssignment(Expression Left, Unit Unit) : Expression {
|
public sealed record UnitAssignment(Expression Left, Unit Unit) : Expression {
|
||||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||||
return visitor.VisitUnitAssignment(this);
|
return visitor.VisitUnitAssignment(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record UnitConversion(Expression Left, Unit Unit) : Expression {
|
public sealed record UnitConversion(Expression Left, ImmutableArray<Unit> Units) : Expression {
|
||||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||||
return visitor.VisitUnitConversion(this);
|
return visitor.VisitUnitConversion(this);
|
||||||
}
|
}
|
||||||
|
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
public interface ExpressionVisitor<T> {
|
public interface ExpressionVisitor<T> {
|
||||||
T VisitNumber(Expression.Number number);
|
T VisitNumber(Expression.Number number);
|
||||||
|
|
||||||
T VisitNumbersWithUnits(Expression.NumbersWithUnits numbersWithUnits);
|
T VisitNumbersWithUnits(Expression.NumbersWithUnits numbersWithUnits);
|
||||||
|
|
||||||
T VisitGrouping(Expression.Grouping grouping);
|
T VisitGrouping(Expression.Grouping grouping);
|
||||||
|
|
||||||
T VisitUnary(Expression.Unary unary);
|
T VisitUnary(Expression.Unary unary);
|
||||||
|
|
||||||
T VisitBinary(Expression.Binary binary);
|
T VisitBinary(Expression.Binary binary);
|
||||||
|
|
||||||
T VisitUnitAssignment(Expression.UnitAssignment unitAssignment);
|
T VisitUnitAssignment(Expression.UnitAssignment unitAssignment);
|
||||||
|
|
||||||
T VisitUnitConversion(Expression.UnitConversion unitConversion);
|
T VisitUnitConversion(Expression.UnitConversion unitConversion);
|
||||||
}
|
}
|
||||||
|
@@ -7,94 +7,89 @@ namespace Calculator.Parser;
|
|||||||
|
|
||||||
public sealed class Parser(ImmutableArray<Token> tokens) {
|
public sealed class Parser(ImmutableArray<Token> tokens) {
|
||||||
private int current = 0;
|
private int current = 0;
|
||||||
|
|
||||||
private bool IsEOF => current >= tokens.Length;
|
private bool IsEOF => current >= tokens.Length;
|
||||||
|
|
||||||
private static readonly ImmutableArray<SimpleTokenType> PLUS_MINUS = [
|
private static readonly ImmutableArray<SimpleTokenType> PLUS_MINUS = [
|
||||||
SimpleTokenType.PLUS,
|
SimpleTokenType.PLUS,
|
||||||
SimpleTokenType.MINUS
|
SimpleTokenType.MINUS,
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly ImmutableArray<SimpleTokenType> STAR_SLASH_PERCENT = [
|
private static readonly ImmutableArray<SimpleTokenType> STAR_SLASH_PERCENT = [
|
||||||
SimpleTokenType.STAR,
|
SimpleTokenType.STAR,
|
||||||
SimpleTokenType.SLASH,
|
SimpleTokenType.SLASH,
|
||||||
SimpleTokenType.PERCENT
|
SimpleTokenType.PERCENT,
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly ImmutableArray<SimpleTokenType> CARET = [
|
private static readonly ImmutableArray<SimpleTokenType> CARET = [
|
||||||
SimpleTokenType.CARET
|
SimpleTokenType.CARET,
|
||||||
];
|
];
|
||||||
|
|
||||||
private bool Match(SimpleTokenType expectedTokenType, [NotNullWhen(true)] out Token.Simple? token) {
|
private bool Match(SimpleTokenType expectedTokenType, [NotNullWhen(true)] out Token.Simple? token) {
|
||||||
return Match(simpleToken => simpleToken.Type == expectedTokenType, out token);
|
return Match(simpleToken => simpleToken.Type == expectedTokenType, out token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Match(ImmutableArray<SimpleTokenType> expectedTokenTypes, [NotNullWhen(true)] out Token.Simple? token) {
|
private bool Match(ImmutableArray<SimpleTokenType> expectedTokenTypes, [NotNullWhen(true)] out Token.Simple? token) {
|
||||||
return Match(simpleToken => expectedTokenTypes.Contains(simpleToken.Type), out token);
|
return Match(simpleToken => expectedTokenTypes.Contains(simpleToken.Type), out token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Match<T>([NotNullWhen(true)] out T? token) where T : Token {
|
private bool Match<T>([NotNullWhen(true)] out T? token) where T : Token {
|
||||||
return Match(static _ => true, out token);
|
return Match(static _ => true, out token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Match<T>(Predicate<T> predicate, [NotNullWhen(true)] out T? token) where T : 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)) {
|
if (!IsEOF && tokens[current] is T t && predicate(t)) {
|
||||||
current++;
|
current++;
|
||||||
token = t;
|
token = t;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
token = null;
|
token = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Expression Parse() {
|
public Expression Parse() {
|
||||||
Expression term = Term();
|
Expression term = Term();
|
||||||
|
|
||||||
|
if (Match<Token.Text>(static text => text.Value is "to" or "in", out _)) {
|
||||||
|
if (MatchOneOrMoreUnits(out ImmutableArray<Unit> units)) {
|
||||||
|
term = new Expression.UnitConversion(term, units);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new ParseException("Expected one or more unit literals");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsEOF) {
|
if (!IsEOF) {
|
||||||
throw new ParseException("Incomplete expression");
|
throw new ParseException("Incomplete expression");
|
||||||
}
|
}
|
||||||
|
|
||||||
return term;
|
return term;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expression Term() {
|
private Expression Term() {
|
||||||
return Binary(Factor, PLUS_MINUS);
|
return Binary(Factor, PLUS_MINUS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expression Factor() {
|
private Expression Factor() {
|
||||||
return Binary(Exponentiation, STAR_SLASH_PERCENT);
|
return Binary(Exponentiation, STAR_SLASH_PERCENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expression Exponentiation() {
|
private Expression Exponentiation() {
|
||||||
return Binary(Conversion, CARET);
|
return Binary(Unary, CARET);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expression Binary(Func<Expression> term, ImmutableArray<SimpleTokenType> expectedTokenTypes) {
|
private Expression Binary(Func<Expression> term, ImmutableArray<SimpleTokenType> expectedTokenTypes) {
|
||||||
Expression left = term();
|
Expression left = term();
|
||||||
|
|
||||||
while (Match(expectedTokenTypes, out Token.Simple? op)) {
|
while (Match(expectedTokenTypes, out Token.Simple? op)) {
|
||||||
Expression right = term();
|
Expression right = term();
|
||||||
left = new Expression.Binary(left, op, right);
|
left = new Expression.Binary(left, op, right);
|
||||||
}
|
}
|
||||||
|
|
||||||
return left;
|
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() {
|
private Expression Unary() {
|
||||||
if (Match(PLUS_MINUS, out Token.Simple? op)) {
|
if (Match(PLUS_MINUS, out Token.Simple? op)) {
|
||||||
Expression right = Unary();
|
Expression right = Unary();
|
||||||
@@ -104,65 +99,54 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
|
|||||||
return Primary();
|
return Primary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expression Primary() {
|
private Expression Primary() {
|
||||||
if (Match(LiteralPredicate, out Token? literal)) {
|
if (Match(LiteralPredicate, out Token? literal)) {
|
||||||
if (literal is not Token.Number number) {
|
if (literal is not Token.Number number) {
|
||||||
throw new ParseException("Expected a number literal");
|
throw new ParseException("Expected a number literal");
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression expression;
|
Expression expression;
|
||||||
|
|
||||||
if (!MatchUnit(out Unit? unit)) {
|
if (!MatchUnit(out Unit? unit)) {
|
||||||
expression = new Expression.Number(number);
|
expression = new Expression.Number(number);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var numbersWithUnits = ImmutableArray.CreateBuilder<(Token.Number, Unit)>();
|
var numbersWithUnits = ImmutableArray.CreateBuilder<(Token.Number, Unit)>();
|
||||||
numbersWithUnits.Add((number, unit));
|
numbersWithUnits.Add((number, unit));
|
||||||
|
|
||||||
while (MatchNumberWithUnit(out var numberWithUnit)) {
|
while (MatchNumberWithUnit(out var numberWithUnit)) {
|
||||||
numbersWithUnits.Add(numberWithUnit.Value);
|
numbersWithUnits.Add(numberWithUnit.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
expression = new Expression.NumbersWithUnits(numbersWithUnits.ToImmutable());
|
expression = new Expression.NumbersWithUnits(numbersWithUnits.ToImmutable());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
|
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
|
||||||
expression = new Expression.Binary(expression, new Token.Simple(SimpleTokenType.STAR), InsideParentheses());
|
expression = new Expression.Binary(expression, new Token.Simple(SimpleTokenType.STAR), InsideParentheses());
|
||||||
}
|
}
|
||||||
|
|
||||||
return expression;
|
return expression;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
|
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
|
||||||
return new Expression.Grouping(InsideParentheses());
|
return new Expression.Grouping(InsideParentheses());
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ParseException("Unexpected token type: " + tokens[current]);
|
throw new ParseException("Unexpected token type: " + tokens[current]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool LiteralPredicate(Token token) {
|
private static bool LiteralPredicate(Token token) {
|
||||||
return token is Token.Text or Token.Number;
|
return token is Token.Text or Token.Number;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Expression InsideParentheses() {
|
private Expression InsideParentheses() {
|
||||||
Expression term = Term();
|
Expression term = Term();
|
||||||
|
|
||||||
if (!Match(SimpleTokenType.RIGHT_PARENTHESIS, out _)) {
|
if (!Match(SimpleTokenType.RIGHT_PARENTHESIS, out _)) {
|
||||||
throw new ParseException("Expected ')' after expression.");
|
throw new ParseException("Expected ')' after expression.");
|
||||||
}
|
}
|
||||||
|
|
||||||
int position = current;
|
|
||||||
|
|
||||||
if (MatchUnitConversionOperator()) {
|
|
||||||
if (MatchUnit(out Unit? toUnit)) {
|
|
||||||
return new Expression.UnitConversion(term, toUnit);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
current = position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MatchUnit(out Unit? unit)) {
|
if (MatchUnit(out Unit? unit)) {
|
||||||
return new Expression.UnitAssignment(term, unit);
|
return new Expression.UnitAssignment(term, unit);
|
||||||
}
|
}
|
||||||
@@ -170,33 +154,50 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
|
|||||||
return term;
|
return term;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool MatchNumberWithUnit([NotNullWhen(true)] out (Token.Number, Unit)? numberWithUnit) {
|
private bool MatchNumberWithUnit([NotNullWhen(true)] out (Token.Number, Unit)? numberWithUnit) {
|
||||||
if (!Match(out Token.Number? number)) {
|
if (!Match(out Token.Number? number)) {
|
||||||
numberWithUnit = null;
|
numberWithUnit = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MatchUnit(out Unit? unit)) {
|
if (!MatchUnit(out Unit? unit)) {
|
||||||
throw new ParseException("Expected a unit literal");
|
throw new ParseException("Expected a unit literal");
|
||||||
}
|
}
|
||||||
|
|
||||||
numberWithUnit = (number, unit);
|
numberWithUnit = (number, unit);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool MatchOneOrMoreUnits(out ImmutableArray<Unit> units) {
|
||||||
|
if (!MatchUnit(out Unit? nextUnit)) {
|
||||||
|
units = ImmutableArray<Unit>.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = ImmutableArray.CreateBuilder<Unit>();
|
||||||
|
|
||||||
|
do {
|
||||||
|
result.Add(nextUnit);
|
||||||
|
Match<Token.Text>(static text => text.Value is "and", out _);
|
||||||
|
} while (MatchUnit(out nextUnit));
|
||||||
|
|
||||||
|
units = result.ToImmutable();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private bool MatchUnit([NotNullWhen(true)] out Unit? unit) {
|
private bool MatchUnit([NotNullWhen(true)] out Unit? unit) {
|
||||||
int position = current;
|
int position = current;
|
||||||
|
|
||||||
UnitUniverses.WordLookupTrieNode node = Units.All.UnitLookupByWords;
|
UnitUniverses.WordLookupTrieNode node = Units.All.UnitLookupByWords;
|
||||||
|
|
||||||
// ReSharper disable once AccessToModifiedClosure
|
// ReSharper disable once AccessToModifiedClosure
|
||||||
while (Match(token => node.Children.ContainsKey(token.Value), out Token.Text? text)) {
|
while (Match(token => node.Children.ContainsKey(token.Value), out Token.Text? text)) {
|
||||||
node = node.Children[text.Value];
|
node = node.Children[text.Value];
|
||||||
}
|
}
|
||||||
|
|
||||||
unit = node.Unit;
|
unit = node.Unit;
|
||||||
|
|
||||||
if (unit != null) {
|
if (unit != null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -205,8 +206,4 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool MatchUnitConversionOperator() {
|
|
||||||
return Match<Token.Text>(static text => text.Value is "to" or "in", out _);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@ public enum SimpleTokenType {
|
|||||||
SLASH,
|
SLASH,
|
||||||
PERCENT,
|
PERCENT,
|
||||||
CARET,
|
CARET,
|
||||||
|
|
||||||
LEFT_PARENTHESIS,
|
LEFT_PARENTHESIS,
|
||||||
RIGHT_PARENTHESIS
|
RIGHT_PARENTHESIS
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ namespace Calculator.Parser;
|
|||||||
|
|
||||||
public abstract record Token {
|
public abstract record Token {
|
||||||
private Token() {}
|
private Token() {}
|
||||||
|
|
||||||
public sealed record Simple(SimpleTokenType Type) : 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.
|
#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() {
|
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.
|
#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 sealed record Text(string Value) : Token {
|
||||||
public override string ToString() {
|
public override string ToString() {
|
||||||
return Value;
|
return Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record Number(Math.Number Value) : Token {
|
public sealed record Number(Math.Number Value) : Token {
|
||||||
public override string ToString() {
|
public override string ToString() {
|
||||||
return Value.ToString(CultureInfo.InvariantCulture);
|
return Value.ToString(CultureInfo.InvariantCulture);
|
||||||
|
@@ -9,114 +9,114 @@ namespace Calculator.Parser;
|
|||||||
|
|
||||||
public sealed class Tokenizer(string input) {
|
public sealed class Tokenizer(string input) {
|
||||||
private int position = 0;
|
private int position = 0;
|
||||||
|
|
||||||
private bool IsEOF => position >= input.Length;
|
private bool IsEOF => position >= input.Length;
|
||||||
|
|
||||||
private char Advance() {
|
private char Advance() {
|
||||||
return input[position++];
|
return input[position++];
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Match(char c) {
|
private bool Match(char c) {
|
||||||
return Match(found => found == c, out _);
|
return Match(found => found == c, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool Match(Predicate<char> predicate, out char c) {
|
private bool Match(Predicate<char> predicate, out char c) {
|
||||||
if (IsEOF) {
|
if (IsEOF) {
|
||||||
c = default;
|
c = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
c = input[position];
|
c = input[position];
|
||||||
|
|
||||||
if (!predicate(c)) {
|
if (!predicate(c)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
position++;
|
position++;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MatchWhile(StringBuilder result, Predicate<char> predicate) {
|
private void MatchWhile(StringBuilder result, Predicate<char> predicate) {
|
||||||
while (Match(predicate, out char c)) {
|
while (Match(predicate, out char c)) {
|
||||||
result.Append(c);
|
result.Append(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string MatchWhile(Predicate<char> predicate) {
|
private string MatchWhile(Predicate<char> predicate) {
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
MatchWhile(result, predicate);
|
MatchWhile(result, predicate);
|
||||||
return result.ToString();
|
return result.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string MatchRest(char firstChar, Predicate<char> predicate) {
|
private string MatchRest(char firstChar, Predicate<char> predicate) {
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
result.Append(firstChar);
|
result.Append(firstChar);
|
||||||
MatchWhile(result, predicate);
|
MatchWhile(result, predicate);
|
||||||
return result.ToString();
|
return result.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImmutableArray<Token> Scan() {
|
public ImmutableArray<Token> Scan() {
|
||||||
ImmutableArray<Token>.Builder tokens = ImmutableArray.CreateBuilder<Token>();
|
ImmutableArray<Token>.Builder tokens = ImmutableArray.CreateBuilder<Token>();
|
||||||
|
|
||||||
void AddToken(Token token) {
|
void AddToken(Token token) {
|
||||||
tokens.Add(token);
|
tokens.Add(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddSimpleToken(SimpleTokenType tokenType) {
|
void AddSimpleToken(SimpleTokenType tokenType) {
|
||||||
AddToken(new Token.Simple(tokenType));
|
AddToken(new Token.Simple(tokenType));
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!IsEOF) {
|
while (!IsEOF) {
|
||||||
char c = Advance();
|
char c = Advance();
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case ' ':
|
case ' ':
|
||||||
// Ignore whitespace.
|
// Ignore whitespace.
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '+':
|
case '+':
|
||||||
AddSimpleToken(SimpleTokenType.PLUS);
|
AddSimpleToken(SimpleTokenType.PLUS);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '-':
|
case '-':
|
||||||
AddSimpleToken(SimpleTokenType.MINUS);
|
AddSimpleToken(SimpleTokenType.MINUS);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '*':
|
case '*':
|
||||||
AddSimpleToken(SimpleTokenType.STAR);
|
AddSimpleToken(SimpleTokenType.STAR);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '/':
|
case '/':
|
||||||
AddSimpleToken(SimpleTokenType.SLASH);
|
AddSimpleToken(SimpleTokenType.SLASH);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '%':
|
case '%':
|
||||||
AddSimpleToken(SimpleTokenType.PERCENT);
|
AddSimpleToken(SimpleTokenType.PERCENT);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '^':
|
case '^':
|
||||||
AddSimpleToken(SimpleTokenType.CARET);
|
AddSimpleToken(SimpleTokenType.CARET);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '(':
|
case '(':
|
||||||
AddSimpleToken(SimpleTokenType.LEFT_PARENTHESIS);
|
AddSimpleToken(SimpleTokenType.LEFT_PARENTHESIS);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ')':
|
case ')':
|
||||||
AddSimpleToken(SimpleTokenType.RIGHT_PARENTHESIS);
|
AddSimpleToken(SimpleTokenType.RIGHT_PARENTHESIS);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '"' or '\'':
|
case '"' or '\'':
|
||||||
AddToken(new Token.Text(c.ToString()));
|
AddToken(new Token.Text(c.ToString()));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case '°':
|
case '°':
|
||||||
case {} when char.IsLetter(c):
|
case {} when char.IsLetter(c):
|
||||||
AddToken(new Token.Text(MatchRest(c, char.IsLetterOrDigit)));
|
AddToken(new Token.Text(MatchRest(c, char.IsLetterOrDigit)));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case {} when char.IsAsciiDigit(c):
|
case {} when char.IsAsciiDigit(c):
|
||||||
string integerPart = MatchRest(c, char.IsAsciiDigit);
|
string integerPart = MatchRest(c, char.IsAsciiDigit);
|
||||||
|
|
||||||
if (Match('.')) {
|
if (Match('.')) {
|
||||||
string fractionalPart = MatchWhile(char.IsAsciiDigit);
|
string fractionalPart = MatchWhile(char.IsAsciiDigit);
|
||||||
AddToken(new Token.Number(ParseNumber(integerPart, fractionalPart)));
|
AddToken(new Token.Number(ParseNumber(integerPart, fractionalPart)));
|
||||||
@@ -124,17 +124,17 @@ public sealed class Tokenizer(string input) {
|
|||||||
else {
|
else {
|
||||||
AddToken(new Token.Number(ParseNumber(integerPart)));
|
AddToken(new Token.Number(ParseNumber(integerPart)));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new TokenizationException("Unexpected character: " + c, c);
|
throw new TokenizationException("Unexpected character: " + c, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokens.ToImmutable();
|
return tokens.ToImmutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static BigRational ParseNumber(string integerPart, string? fractionalPart = null) {
|
internal static BigRational ParseNumber(string integerPart, string? fractionalPart = null) {
|
||||||
if (fractionalPart == null) {
|
if (fractionalPart == null) {
|
||||||
return new BigRational(BigInteger.Parse(integerPart, NumberStyles.Integer, CultureInfo.InvariantCulture));
|
return new BigRational(BigInteger.Parse(integerPart, NumberStyles.Integer, CultureInfo.InvariantCulture));
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>chylex</Authors>
|
<Authors>chylex</Authors>
|
||||||
<Version>2.0.0.0</Version>
|
<Version>2.3.0.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@@ -11,7 +11,7 @@ sealed class CalculatorApp : IApp {
|
|||||||
ImmutableArray<Token> tokens = new Tokenizer(command).Scan();
|
ImmutableArray<Token> tokens = new Tokenizer(command).Scan();
|
||||||
Expression expression = new Parser(tokens).Parse();
|
Expression expression = new Parser(tokens).Parse();
|
||||||
NumberWithUnit result = expression.Accept(new CalculatorExpressionVisitor());
|
NumberWithUnit result = expression.Accept(new CalculatorExpressionVisitor());
|
||||||
|
|
||||||
output = result.ToString();
|
output = result.ToString();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@@ -14,11 +14,11 @@ sealed class KillProcessApp : IApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int succeeded = 0, failed = 0;
|
int succeeded = 0, failed = 0;
|
||||||
|
|
||||||
foreach (string processName in args[1..]) {
|
foreach (string processName in args[1..]) {
|
||||||
try {
|
try {
|
||||||
Process[] processes = Process.GetProcessesByName(processName.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase) ? processName[..^4] : processName);
|
Process[] processes = Process.GetProcessesByName(processName.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase) ? processName[..^4] : processName);
|
||||||
|
|
||||||
foreach (Process process in processes) {
|
foreach (Process process in processes) {
|
||||||
try {
|
try {
|
||||||
process.Kill();
|
process.Kill();
|
||||||
@@ -26,21 +26,21 @@ sealed class KillProcessApp : IApp {
|
|||||||
} catch {
|
} catch {
|
||||||
++failed;
|
++failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.Close();
|
process.Close();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
++failed;
|
++failed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var build = new StringBuilder();
|
var build = new StringBuilder();
|
||||||
build.Append("Killed ").Append(succeeded).Append(" process").Append(succeeded == 1 ? "" : "es");
|
build.Append("Killed ").Append(succeeded).Append(" process").Append(succeeded == 1 ? "" : "es");
|
||||||
|
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
build.Append(", failed ").Append(failed);
|
build.Append(", failed ").Append(failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
output = build.Append('.').ToString();
|
output = build.Append('.').ToString();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ sealed class MemeApp : IApp {
|
|||||||
{ "flip", @"(╯°□°)╯︵ ┻━┻" },
|
{ "flip", @"(╯°□°)╯︵ ┻━┻" },
|
||||||
{ "tableflip", @"(╯°□°)╯︵ ┻━┻" }
|
{ "tableflip", @"(╯°□°)╯︵ ┻━┻" }
|
||||||
};
|
};
|
||||||
|
|
||||||
public bool TryRun(string command, [NotNullWhen(true)] out string? output) {
|
public bool TryRun(string command, [NotNullWhen(true)] out string? output) {
|
||||||
return Map.TryGetValue(command, out output);
|
return Map.TryGetValue(command, out output);
|
||||||
}
|
}
|
||||||
|
@@ -5,19 +5,19 @@ namespace Query.Command;
|
|||||||
sealed class CommandHistory {
|
sealed class CommandHistory {
|
||||||
private readonly List<string> queries = [];
|
private readonly List<string> queries = [];
|
||||||
private readonly List<string> results = [];
|
private readonly List<string> results = [];
|
||||||
|
|
||||||
public IList<string> Queries => queries;
|
public IList<string> Queries => queries;
|
||||||
|
|
||||||
public IList<string> Results => results;
|
public IList<string> Results => results;
|
||||||
|
|
||||||
public void AddQuery(string text) {
|
public void AddQuery(string text) {
|
||||||
queries.Add(text);
|
queries.Add(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddResult(string text) {
|
public void AddResult(string text) {
|
||||||
results.Add(text);
|
results.Add(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear() {
|
public void Clear() {
|
||||||
queries.Clear();
|
queries.Clear();
|
||||||
results.Clear();
|
results.Clear();
|
||||||
|
@@ -10,7 +10,7 @@ sealed class CommandProcessor {
|
|||||||
new KillProcessApp(),
|
new KillProcessApp(),
|
||||||
new CalculatorApp()
|
new CalculatorApp()
|
||||||
];
|
];
|
||||||
|
|
||||||
public string Run(string command) {
|
public string Run(string command) {
|
||||||
try {
|
try {
|
||||||
foreach (IApp app in apps) {
|
foreach (IApp app in apps) {
|
||||||
@@ -18,7 +18,7 @@ sealed class CommandProcessor {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Unknown command.";
|
return "Unknown command.";
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new CommandException(e.Message, e);
|
throw new CommandException(e.Message, e);
|
||||||
|
@@ -6,57 +6,57 @@ namespace Query.Form;
|
|||||||
|
|
||||||
sealed class KeyboardHook {
|
sealed class KeyboardHook {
|
||||||
public event EventHandler? Triggered;
|
public event EventHandler? Triggered;
|
||||||
|
|
||||||
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
|
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
|
||||||
private readonly NativeMethods.HookProc keyboardHookDelegate;
|
private readonly NativeMethods.HookProc keyboardHookDelegate;
|
||||||
private IntPtr keyboardHook;
|
private IntPtr keyboardHook;
|
||||||
|
|
||||||
public KeyboardHook() {
|
public KeyboardHook() {
|
||||||
keyboardHookDelegate = KeyboardHookProc;
|
keyboardHookDelegate = KeyboardHookProc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StartHook() {
|
public void StartHook() {
|
||||||
if (keyboardHook != IntPtr.Zero) {
|
if (keyboardHook != IntPtr.Zero) {
|
||||||
NativeMethods.UnhookWindowsHookEx(keyboardHook);
|
NativeMethods.UnhookWindowsHookEx(keyboardHook);
|
||||||
}
|
}
|
||||||
|
|
||||||
keyboardHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_KEYBOARD_LL, keyboardHookDelegate, IntPtr.Zero, 0);
|
keyboardHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_KEYBOARD_LL, keyboardHookDelegate, IntPtr.Zero, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopHook() {
|
public void StopHook() {
|
||||||
if (keyboardHook != IntPtr.Zero) {
|
if (keyboardHook != IntPtr.Zero) {
|
||||||
NativeMethods.UnhookWindowsHookEx(keyboardHook);
|
NativeMethods.UnhookWindowsHookEx(keyboardHook);
|
||||||
keyboardHook = IntPtr.Zero;
|
keyboardHook = IntPtr.Zero;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam) {
|
private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam) {
|
||||||
if (wParam == NativeMethods.WM_KEYDOWN) {
|
if (wParam == NativeMethods.WM_KEYDOWN) {
|
||||||
Keys key = (Keys) Marshal.ReadInt32(lParam);
|
Keys key = (Keys) Marshal.ReadInt32(lParam);
|
||||||
|
|
||||||
if (key is Keys.LWin or Keys.RWin && Control.ModifierKeys.HasFlag(Keys.Control)) {
|
if (key is Keys.LWin or Keys.RWin && Control.ModifierKeys.HasFlag(Keys.Control)) {
|
||||||
Triggered?.Invoke(this, EventArgs.Empty);
|
Triggered?.Invoke(this, EventArgs.Empty);
|
||||||
return NativeMethods.HookHandled;
|
return NativeMethods.HookHandled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NativeMethods.CallNextHookEx(keyboardHook, nCode, wParam, lParam);
|
return NativeMethods.CallNextHookEx(keyboardHook, nCode, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class NativeMethods {
|
private static class NativeMethods {
|
||||||
public const int WH_KEYBOARD_LL = 13;
|
public const int WH_KEYBOARD_LL = 13;
|
||||||
public const int WM_KEYDOWN = 0x0100;
|
public const int WM_KEYDOWN = 0x0100;
|
||||||
|
|
||||||
public static readonly IntPtr HookHandled = new (-1);
|
public static readonly IntPtr HookHandled = new (-1);
|
||||||
|
|
||||||
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
|
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);
|
public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
public static extern bool UnhookWindowsHookEx(IntPtr idHook);
|
public static extern bool UnhookWindowsHookEx(IntPtr idHook);
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);
|
public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);
|
||||||
}
|
}
|
||||||
|
@@ -64,7 +64,7 @@ sealed partial class MainForm : System.Windows.Forms.Form {
|
|||||||
private bool isMoving = false;
|
private bool isMoving = false;
|
||||||
|
|
||||||
private void FixLocation() {
|
private void FixLocation() {
|
||||||
if (!isMoving && Screen.FromControl(this) is {} screen) {
|
if (!isMoving && WindowState != FormWindowState.Minimized && Screen.FromControl(this) is {} screen) {
|
||||||
Rectangle screenRect = screen.WorkingArea;
|
Rectangle screenRect = screen.WorkingArea;
|
||||||
isMoving = true;
|
isMoving = true;
|
||||||
Location = new Point(screenRect.X + screenRect.Width - Width, screenRect.Y + screenRect.Height - Height);
|
Location = new Point(screenRect.X + screenRect.Width - Width, screenRect.Y + screenRect.Height - Height);
|
||||||
@@ -104,8 +104,10 @@ sealed partial class MainForm : System.Windows.Forms.Form {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void focusTimer_Tick(object? sender, EventArgs e) {
|
private void focusTimer_Tick(object? sender, EventArgs e) {
|
||||||
|
WindowState = FormWindowState.Minimized;
|
||||||
Show();
|
Show();
|
||||||
Activate();
|
Activate();
|
||||||
|
WindowState = FormWindowState.Normal;
|
||||||
|
|
||||||
queryBox.Focus();
|
queryBox.Focus();
|
||||||
focusTimer.Stop();
|
focusTimer.Stop();
|
||||||
|
@@ -157,4 +157,4 @@
|
|||||||
AEE=
|
AEE=
|
||||||
</value>
|
</value>
|
||||||
</data>
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
@@ -11,21 +11,21 @@ sealed partial class QueryHistoryLog : UserControl {
|
|||||||
Information,
|
Information,
|
||||||
Error
|
Error
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly Dictionary<EntryType, Color> EntryColorMap = new () {
|
private static readonly Dictionary<EntryType, Color> EntryColorMap = new () {
|
||||||
{ EntryType.UserInput, Color.FromArgb(160, 160, 160) },
|
{ EntryType.UserInput, Color.FromArgb(160, 160, 160) },
|
||||||
{ EntryType.CommandResult, Color.FromArgb(240, 240, 240) },
|
{ EntryType.CommandResult, Color.FromArgb(240, 240, 240) },
|
||||||
{ EntryType.Information, Color.FromArgb(160, 255, 140) },
|
{ EntryType.Information, Color.FromArgb(160, 255, 140) },
|
||||||
{ EntryType.Error, Color.FromArgb(255, 40, 40) }
|
{ EntryType.Error, Color.FromArgb(255, 40, 40) }
|
||||||
};
|
};
|
||||||
|
|
||||||
public QueryHistoryLog() {
|
public QueryHistoryLog() {
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddEntry(string text, EntryType type) {
|
public void AddEntry(string text, EntryType type) {
|
||||||
int width = container.Width - SystemInformation.VerticalScrollBarWidth;
|
int width = container.Width - SystemInformation.VerticalScrollBarWidth;
|
||||||
|
|
||||||
Label label = new Label {
|
Label label = new Label {
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
Font = container.Font,
|
Font = container.Font,
|
||||||
@@ -35,11 +35,11 @@ sealed partial class QueryHistoryLog : UserControl {
|
|||||||
MaximumSize = new Size(width, 0),
|
MaximumSize = new Size(width, 0),
|
||||||
Width = width
|
Width = width
|
||||||
};
|
};
|
||||||
|
|
||||||
container.Controls.Add(label);
|
container.Controls.Add(label);
|
||||||
container.AutoScrollPosition = new Point(0, container.VerticalScroll.Maximum);
|
container.AutoScrollPosition = new Point(0, container.VerticalScroll.Maximum);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearEntries() {
|
public void ClearEntries() {
|
||||||
container.Controls.Clear();
|
container.Controls.Clear();
|
||||||
}
|
}
|
||||||
|
@@ -8,84 +8,84 @@ namespace Query.Form;
|
|||||||
|
|
||||||
sealed partial class QueryTextBox : UserControl {
|
sealed partial class QueryTextBox : UserControl {
|
||||||
public event EventHandler<CommandEventArgs>? CommandRan;
|
public event EventHandler<CommandEventArgs>? CommandRan;
|
||||||
|
|
||||||
private CommandHistory history = null!;
|
private CommandHistory history = null!;
|
||||||
private Action<string> log = null!;
|
private Action<string> log = null!;
|
||||||
|
|
||||||
public QueryTextBox() {
|
public QueryTextBox() {
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Setup(CommandHistory historyObj, Action<string> logFunc) {
|
public void Setup(CommandHistory historyObj, Action<string> logFunc) {
|
||||||
history = historyObj;
|
history = historyObj;
|
||||||
log = logFunc;
|
log = logFunc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCommandRan() {
|
private void OnCommandRan() {
|
||||||
CommandRan?.Invoke(this, new CommandEventArgs(tb.Text));
|
CommandRan?.Invoke(this, new CommandEventArgs(tb.Text));
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class CustomTextBox : TextBox {
|
private sealed class CustomTextBox : TextBox {
|
||||||
private string lastInputStr = string.Empty;
|
private string lastInputStr = string.Empty;
|
||||||
private int lastInputPos = 0;
|
private int lastInputPos = 0;
|
||||||
|
|
||||||
private bool doResetHistoryMemory;
|
private bool doResetHistoryMemory;
|
||||||
private bool lastArrowShift;
|
private bool lastArrowShift;
|
||||||
private int historyOffset;
|
private int historyOffset;
|
||||||
|
|
||||||
public CustomTextBox() {
|
public CustomTextBox() {
|
||||||
TextChanged += CustomTextBox_TextChanged;
|
TextChanged += CustomTextBox_TextChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnKeyDown(KeyEventArgs e) {
|
protected override void OnKeyDown(KeyEventArgs e) {
|
||||||
QueryTextBox input = (QueryTextBox) Parent!;
|
QueryTextBox input = (QueryTextBox) Parent!;
|
||||||
CommandHistory history = input.history;
|
CommandHistory history = input.history;
|
||||||
|
|
||||||
Keys key = e.KeyCode;
|
Keys key = e.KeyCode;
|
||||||
bool handled = false;
|
bool handled = false;
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case Keys.Enter:
|
case Keys.Enter:
|
||||||
if (Text != string.Empty) {
|
if (Text != string.Empty) {
|
||||||
input.OnCommandRan();
|
input.OnCommandRan();
|
||||||
|
|
||||||
Text = string.Empty;
|
Text = string.Empty;
|
||||||
doResetHistoryMemory = true;
|
doResetHistoryMemory = true;
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Keys.Up:
|
case Keys.Up:
|
||||||
if (lastArrowShift != e.Shift) {
|
if (lastArrowShift != e.Shift) {
|
||||||
lastArrowShift = e.Shift;
|
lastArrowShift = e.Shift;
|
||||||
historyOffset = 0;
|
historyOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
--historyOffset;
|
--historyOffset;
|
||||||
|
|
||||||
if (InsertFromHistory(e.Shift ? history.Results : history.Queries)) {
|
if (InsertFromHistory(e.Shift ? history.Results : history.Queries)) {
|
||||||
++historyOffset;
|
++historyOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Keys.Down:
|
case Keys.Down:
|
||||||
if (lastArrowShift != e.Shift) {
|
if (lastArrowShift != e.Shift) {
|
||||||
lastArrowShift = e.Shift;
|
lastArrowShift = e.Shift;
|
||||||
historyOffset = 0;
|
historyOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
++historyOffset;
|
++historyOffset;
|
||||||
|
|
||||||
if (InsertFromHistory(e.Shift ? history.Results : history.Queries)) {
|
if (InsertFromHistory(e.Shift ? history.Results : history.Queries)) {
|
||||||
--historyOffset;
|
--historyOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
handled = true;
|
handled = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Keys.C:
|
case Keys.C:
|
||||||
if (e.Modifiers == Keys.Control) {
|
if (e.Modifiers == Keys.Control) {
|
||||||
if (SelectionLength == 0 && history.Results.Count > 0) {
|
if (SelectionLength == 0 && history.Results.Count > 0) {
|
||||||
@@ -94,47 +94,47 @@ sealed partial class QueryTextBox : UserControl {
|
|||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!handled && key != Keys.ControlKey && key != Keys.ShiftKey && key != Keys.Menu) {
|
if (!handled && key != Keys.ControlKey && key != Keys.ShiftKey && key != Keys.Menu) {
|
||||||
doResetHistoryMemory = true;
|
doResetHistoryMemory = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Handled = e.SuppressKeyPress = handled;
|
e.Handled = e.SuppressKeyPress = handled;
|
||||||
base.OnKeyDown(e);
|
base.OnKeyDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnKeyUp(KeyEventArgs e) {
|
protected override void OnKeyUp(KeyEventArgs e) {
|
||||||
base.OnKeyUp(e);
|
base.OnKeyUp(e);
|
||||||
|
|
||||||
if (doResetHistoryMemory) {
|
if (doResetHistoryMemory) {
|
||||||
doResetHistoryMemory = false;
|
doResetHistoryMemory = false;
|
||||||
ResetHistoryMemory();
|
ResetHistoryMemory();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CustomTextBox_TextChanged(object? sender, EventArgs e) {
|
private void CustomTextBox_TextChanged(object? sender, EventArgs e) {
|
||||||
ResetHistoryMemory();
|
ResetHistoryMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Management
|
// Management
|
||||||
|
|
||||||
private void ResetHistoryMemory() {
|
private void ResetHistoryMemory() {
|
||||||
lastInputStr = Text;
|
lastInputStr = Text;
|
||||||
lastInputPos = SelectionStart;
|
lastInputPos = SelectionStart;
|
||||||
historyOffset = 0;
|
historyOffset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool InsertFromHistory(IList<string> collection) {
|
private bool InsertFromHistory(IList<string> collection) {
|
||||||
if (collection.Count == 0) {
|
if (collection.Count == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int index = collection.Count + historyOffset;
|
int index = collection.Count + historyOffset;
|
||||||
bool wasClamped = false;
|
bool wasClamped = false;
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
index = 0;
|
index = 0;
|
||||||
wasClamped = true;
|
wasClamped = true;
|
||||||
@@ -143,13 +143,13 @@ sealed partial class QueryTextBox : UserControl {
|
|||||||
index = collection.Count - 1;
|
index = collection.Count - 1;
|
||||||
wasClamped = true;
|
wasClamped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextChanged -= CustomTextBox_TextChanged;
|
TextChanged -= CustomTextBox_TextChanged;
|
||||||
|
|
||||||
Text = lastInputStr.Insert(lastInputPos, collection[index]);
|
Text = lastInputStr.Insert(lastInputPos, collection[index]);
|
||||||
SelectionStart = lastInputPos + collection[index].Length;
|
SelectionStart = lastInputPos + collection[index].Length;
|
||||||
SelectionLength = 0;
|
SelectionLength = 0;
|
||||||
|
|
||||||
TextChanged += CustomTextBox_TextChanged;
|
TextChanged += CustomTextBox_TextChanged;
|
||||||
return wasClamped;
|
return wasClamped;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user