mirror of
https://github.com/chylex/Query.git
synced 2025-05-01 08:34:13 +02:00
Implement new calculator with unit conversions
This commit is contained in:
parent
12c5418443
commit
146b68fa67
11
Calculator/Calculator.csproj
Normal file
11
Calculator/Calculator.csproj
Normal file
@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ExtendedNumerics.BigRational" Version="2023.1000.2.328" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
5
Calculator/CalculatorException.cs
Normal file
5
Calculator/CalculatorException.cs
Normal file
@ -0,0 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace Calculator;
|
||||
|
||||
sealed class CalculatorException(string message) : Exception(message);
|
71
Calculator/CalculatorExpressionVisitor.cs
Normal file
71
Calculator/CalculatorExpressionVisitor.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using Calculator.Math;
|
||||
using Calculator.Parser;
|
||||
|
||||
namespace Calculator;
|
||||
|
||||
public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUnit> {
|
||||
public NumberWithUnit VisitNumber(Expression.Number number) {
|
||||
return new NumberWithUnit(number.NumberToken.Value, null);
|
||||
}
|
||||
|
||||
public NumberWithUnit VisitNumbersWithUnits(Expression.NumbersWithUnits numbersWithUnits) {
|
||||
NumberWithUnit result = new Number.Rational(0);
|
||||
|
||||
foreach ((Token.Number number, Unit unit) in numbersWithUnits.NumberTokensWithUnits) {
|
||||
result += new NumberWithUnit(number.Value, unit);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public NumberWithUnit VisitGrouping(Expression.Grouping grouping) {
|
||||
return Evaluate(grouping.Expression);
|
||||
}
|
||||
|
||||
public NumberWithUnit VisitUnary(Expression.Unary unary) {
|
||||
(Token.Simple op, Expression right) = unary;
|
||||
|
||||
return op.Type switch {
|
||||
SimpleTokenType.PLUS => +Evaluate(right),
|
||||
SimpleTokenType.MINUS => -Evaluate(right),
|
||||
_ => throw new CalculatorException("Unsupported unary operator: " + op.Type)
|
||||
};
|
||||
}
|
||||
|
||||
public NumberWithUnit VisitBinary(Expression.Binary binary) {
|
||||
(Expression left, Token.Simple op, Expression right) = binary;
|
||||
|
||||
return op.Type switch {
|
||||
SimpleTokenType.PLUS => Evaluate(left) + Evaluate(right),
|
||||
SimpleTokenType.MINUS => Evaluate(left) - Evaluate(right),
|
||||
SimpleTokenType.STAR => Evaluate(left) * Evaluate(right),
|
||||
SimpleTokenType.SLASH => Evaluate(left) / Evaluate(right),
|
||||
SimpleTokenType.PERCENT => Evaluate(left) % Evaluate(right),
|
||||
SimpleTokenType.CARET => Evaluate(left).Pow(Evaluate(right)),
|
||||
_ => throw new CalculatorException("Unsupported binary operator: " + op.Type)
|
||||
};
|
||||
}
|
||||
|
||||
public NumberWithUnit VisitUnitAssignment(Expression.UnitAssignment unitAssignment) {
|
||||
(Expression left, Unit right) = unitAssignment;
|
||||
|
||||
NumberWithUnit number = Evaluate(left);
|
||||
|
||||
if (number.Unit is null) {
|
||||
return number with { Unit = right };
|
||||
}
|
||||
else {
|
||||
throw new CalculatorException("Expression already has a unit, cannot assign a new unit: " + right);
|
||||
}
|
||||
}
|
||||
|
||||
public NumberWithUnit VisitUnitConversion(Expression.UnitConversion unitConversion) {
|
||||
(Expression left, Unit unit) = unitConversion;
|
||||
|
||||
return Evaluate(left).ConvertTo(unit);
|
||||
}
|
||||
|
||||
private NumberWithUnit Evaluate(Expression expression) {
|
||||
return expression.Accept(this);
|
||||
}
|
||||
}
|
152
Calculator/Math/Number.cs
Normal file
152
Calculator/Math/Number.cs
Normal file
@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using ExtendedNumerics;
|
||||
|
||||
namespace Calculator.Math;
|
||||
|
||||
[SuppressMessage("ReSharper", "MemberCanBeProtected.Global")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public abstract record Number : IAdditionOperators<Number, Number, Number>,
|
||||
ISubtractionOperators<Number, Number, Number>,
|
||||
IMultiplyOperators<Number, Number, Number>,
|
||||
IDivisionOperators<Number, Number, Number>,
|
||||
IModulusOperators<Number, Number, Number>,
|
||||
IUnaryPlusOperators<Number, Number>,
|
||||
IUnaryNegationOperators<Number, Number>,
|
||||
IAdditiveIdentity<Number, Number.Rational>,
|
||||
IMultiplicativeIdentity<Number, Number.Rational> {
|
||||
protected abstract decimal AsDecimal { get; }
|
||||
|
||||
public abstract Number Pow(Number exponent);
|
||||
|
||||
public abstract string ToString(IFormatProvider? formatProvider);
|
||||
|
||||
public sealed override string ToString() {
|
||||
return ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an integer number with arbitrary precision.
|
||||
/// </summary>
|
||||
public sealed record Rational(BigRational Value) : Number {
|
||||
protected override decimal AsDecimal => (decimal) Value;
|
||||
|
||||
public override Number Pow(Number exponent) {
|
||||
if (exponent is Rational { Value: {} rationalExponent }) {
|
||||
Fraction fractionExponent = rationalExponent.GetImproperFraction();
|
||||
if (fractionExponent.Denominator == 1 && fractionExponent.Numerator >= 0) {
|
||||
try {
|
||||
return new Rational(BigRational.Pow(Value, fractionExponent.Numerator));
|
||||
} catch (OverflowException) {}
|
||||
}
|
||||
}
|
||||
|
||||
return new Decimal(AsDecimal).Pow(exponent);
|
||||
}
|
||||
|
||||
public override string ToString(IFormatProvider? formatProvider) {
|
||||
Fraction fraction = Value.GetImproperFraction();
|
||||
return fraction.Denominator == 1 ? fraction.Numerator.ToString(formatProvider) : AsDecimal.ToString(formatProvider);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a decimal number with limited precision.
|
||||
/// </summary>
|
||||
public sealed record Decimal(decimal Value) : Number {
|
||||
public Decimal(double value) : this((decimal) value) {}
|
||||
|
||||
protected override decimal AsDecimal => Value;
|
||||
|
||||
public override Number Pow(Number exponent) {
|
||||
double doubleValue = (double) Value;
|
||||
double doubleExponent = (double) exponent.AsDecimal;
|
||||
return new Decimal(System.Math.Pow(doubleValue, doubleExponent));
|
||||
}
|
||||
|
||||
public override string ToString(IFormatProvider? formatProvider) {
|
||||
return Value.ToString(formatProvider);
|
||||
}
|
||||
}
|
||||
|
||||
public static implicit operator Number(BigRational value) {
|
||||
return new Rational(value);
|
||||
}
|
||||
|
||||
public static implicit operator Number(int value) {
|
||||
return new Rational(value);
|
||||
}
|
||||
|
||||
public static implicit operator Number(long value) {
|
||||
return new Rational(value);
|
||||
}
|
||||
|
||||
public static Rational AdditiveIdentity => new (BigInteger.Zero);
|
||||
public static Rational MultiplicativeIdentity => new (BigInteger.One);
|
||||
|
||||
public static Number Add(Number left, Number right) {
|
||||
return left + right;
|
||||
}
|
||||
|
||||
public static Number Subtract(Number left, Number right) {
|
||||
return left - right;
|
||||
}
|
||||
|
||||
public static Number Multiply(Number left, Number right) {
|
||||
return left * right;
|
||||
}
|
||||
|
||||
public static Number Divide(Number left, Number right) {
|
||||
return left / right;
|
||||
}
|
||||
|
||||
public static Number Remainder(Number left, Number right) {
|
||||
return left % right;
|
||||
}
|
||||
|
||||
public static Number Pow(Number left, Number right) {
|
||||
return left.Pow(right);
|
||||
}
|
||||
|
||||
public static Number operator +(Number value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static Number operator -(Number value) {
|
||||
return Operate(value, BigRational.Negate, decimal.Negate);
|
||||
}
|
||||
|
||||
public static Number operator +(Number left, Number right) {
|
||||
return Operate(left, right, BigRational.Add, decimal.Add);
|
||||
}
|
||||
|
||||
public static Number operator -(Number left, Number right) {
|
||||
return Operate(left, right, BigRational.Subtract, decimal.Subtract);
|
||||
}
|
||||
|
||||
public static Number operator *(Number left, Number right) {
|
||||
return Operate(left, right, BigRational.Multiply, decimal.Multiply);
|
||||
}
|
||||
|
||||
public static Number operator /(Number left, Number right) {
|
||||
return Operate(left, right, BigRational.Divide, decimal.Divide);
|
||||
}
|
||||
|
||||
public static Number operator %(Number left, Number right) {
|
||||
return Operate(left, right, BigRational.Mod, decimal.Remainder);
|
||||
}
|
||||
|
||||
private static Number Operate(Number value, Func<BigRational, BigRational> rationalOperation, Func<decimal, decimal> decimalOperation) {
|
||||
return value is Rational rational
|
||||
? new Rational(rationalOperation(rational.Value))
|
||||
: new Decimal(decimalOperation(value.AsDecimal));
|
||||
}
|
||||
|
||||
private static Number Operate(Number left, Number right, Func<BigRational, BigRational, BigRational> rationalOperation, Func<decimal, decimal, decimal> decimalOperation) {
|
||||
return left is Rational leftRational && right is Rational rightRational
|
||||
? new Rational(rationalOperation(leftRational.Value, rightRational.Value))
|
||||
: new Decimal(decimalOperation(left.AsDecimal, right.AsDecimal));
|
||||
}
|
||||
}
|
108
Calculator/Math/NumberWithUnit.cs
Normal file
108
Calculator/Math/NumberWithUnit.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Calculator.Math;
|
||||
|
||||
[SuppressMessage("ReSharper", "MemberCanBeInternal")]
|
||||
public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAdditionOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||
ISubtractionOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||
IMultiplyOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||
IDivisionOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||
IModulusOperators<NumberWithUnit, NumberWithUnit, NumberWithUnit>,
|
||||
IUnaryPlusOperators<NumberWithUnit, NumberWithUnit>,
|
||||
IUnaryNegationOperators<NumberWithUnit, NumberWithUnit> {
|
||||
public NumberWithUnit ConvertTo(Unit targetUnit) {
|
||||
if (Unit == null) {
|
||||
return this with { Unit = targetUnit };
|
||||
}
|
||||
else if (Units.All.TryGetUniverse(Unit, out var universe) && universe.TryConvert(Number, Unit, targetUnit, out var converted)) {
|
||||
return new NumberWithUnit(converted, targetUnit);
|
||||
}
|
||||
else {
|
||||
throw new ArithmeticException("Cannot convert '" + Unit + "' to '" + targetUnit + "'");
|
||||
}
|
||||
}
|
||||
|
||||
public string ToString(IFormatProvider? formatProvider) {
|
||||
string number = Number.ToString(formatProvider);
|
||||
return Unit == null ? number : number + " " + Unit;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static implicit operator NumberWithUnit(Number number) {
|
||||
return new NumberWithUnit(number, null);
|
||||
}
|
||||
|
||||
public static NumberWithUnit operator +(NumberWithUnit value) {
|
||||
return value with { Number = +value.Number };
|
||||
}
|
||||
|
||||
public static NumberWithUnit operator -(NumberWithUnit value) {
|
||||
return value with { Number = -value.Number };
|
||||
}
|
||||
|
||||
public static NumberWithUnit operator +(NumberWithUnit left, NumberWithUnit right) {
|
||||
return Operate(left, right, Number.Add, static (leftNumber, leftUnit, rightNumber, rightUnit) => {
|
||||
if (leftUnit == rightUnit) {
|
||||
return new NumberWithUnit(leftNumber + rightNumber, leftUnit);
|
||||
}
|
||||
else if (Units.All.TryGetUniverse(leftUnit, out UnitUniverse? universe) && universe.TryConvert(rightNumber, rightUnit, leftUnit, out Number? rightConverted)) {
|
||||
return new NumberWithUnit(leftNumber + rightConverted, leftUnit);
|
||||
}
|
||||
else {
|
||||
throw new ArithmeticException("Cannot add '" + leftUnit + "' and '" + rightUnit + "'");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static NumberWithUnit operator -(NumberWithUnit left, NumberWithUnit right) {
|
||||
return Operate(left, right, Number.Subtract, static (leftNumber, leftUnit, rightNumber, rightUnit) => {
|
||||
if (leftUnit == rightUnit) {
|
||||
return new NumberWithUnit(leftNumber - rightNumber, leftUnit);
|
||||
}
|
||||
else if (Units.All.TryGetUniverse(leftUnit, out UnitUniverse? universe) && universe.TryConvert(rightNumber, rightUnit, leftUnit, out Number? rightConverted)) {
|
||||
return new NumberWithUnit(leftNumber - rightConverted, leftUnit);
|
||||
}
|
||||
else {
|
||||
throw new ArithmeticException("Cannot subtract '" + leftUnit + "' and '" + rightUnit + "'");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static NumberWithUnit operator *(NumberWithUnit left, NumberWithUnit right) {
|
||||
return OperateWithoutUnits(left, right, Number.Multiply, "Cannot multiply");
|
||||
}
|
||||
|
||||
public static NumberWithUnit operator /(NumberWithUnit left, NumberWithUnit right) {
|
||||
return OperateWithoutUnits(left, right, Number.Divide, "Cannot divide");
|
||||
}
|
||||
|
||||
public static NumberWithUnit operator %(NumberWithUnit left, NumberWithUnit right) {
|
||||
return OperateWithoutUnits(left, right, Number.Remainder, "Cannot modulo");
|
||||
}
|
||||
|
||||
public NumberWithUnit Pow(NumberWithUnit exponent) {
|
||||
return OperateWithoutUnits(this, exponent, Number.Pow, "Cannot exponentiate");
|
||||
}
|
||||
|
||||
private static NumberWithUnit Operate(NumberWithUnit left, NumberWithUnit right, Func<Number, Number, Number> withoutUnitsOperation, Func<Number, Unit, Number, Unit, NumberWithUnit> withUnitsOperation) {
|
||||
if (right.Unit is null) {
|
||||
return left with { Number = withoutUnitsOperation(left.Number, right.Number) };
|
||||
}
|
||||
else if (left.Unit is null) {
|
||||
return right with { Number = withoutUnitsOperation(left.Number, right.Number) };
|
||||
}
|
||||
else {
|
||||
return withUnitsOperation(left.Number, left.Unit, right.Number, right.Unit);
|
||||
}
|
||||
}
|
||||
|
||||
private static NumberWithUnit OperateWithoutUnits(NumberWithUnit left, NumberWithUnit right, Func<Number, Number, Number> withoutUnitsOperation, string messagePrefix) {
|
||||
return Operate(left, right, withoutUnitsOperation, (_, leftUnit, _, rightUnit) => throw new ArithmeticException(messagePrefix + " '" + leftUnit + "' and '" + rightUnit + "'"));
|
||||
}
|
||||
}
|
18
Calculator/Math/Unit.cs
Normal file
18
Calculator/Math/Unit.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Calculator.Math;
|
||||
|
||||
public sealed record Unit(string ShortName, ImmutableArray<string> LongNames) {
|
||||
internal void AssignNamesTo(Dictionary<string, Unit> nameToUnitDictionary) {
|
||||
nameToUnitDictionary.Add(ShortName, this);
|
||||
|
||||
foreach (string longName in LongNames) {
|
||||
nameToUnitDictionary.Add(longName, this);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return ShortName;
|
||||
}
|
||||
}
|
122
Calculator/Math/UnitUniverse.cs
Normal file
122
Calculator/Math/UnitUniverse.cs
Normal file
@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using ExtendedNumerics;
|
||||
|
||||
namespace Calculator.Math;
|
||||
|
||||
sealed class UnitUniverse(
|
||||
Unit primaryUnit,
|
||||
FrozenDictionary<Unit, Func<Number, Number>> unitToConversionToPrimaryUnit,
|
||||
FrozenDictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit
|
||||
) {
|
||||
public ImmutableArray<Unit> AllUnits => unitToConversionToPrimaryUnit.Keys;
|
||||
|
||||
internal bool TryConvert(Number value, Unit fromUnit, Unit toUnit, [NotNullWhen(true)] out Number? converted) {
|
||||
if (fromUnit == toUnit) {
|
||||
converted = value;
|
||||
return true;
|
||||
}
|
||||
else if (unitToConversionToPrimaryUnit.TryGetValue(fromUnit, out var convertToPrimaryUnit) && unitToConversionFromPrimaryUnit.TryGetValue(toUnit, out var convertFromPrimaryUnit)) {
|
||||
converted = convertFromPrimaryUnit(convertToPrimaryUnit(value));
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
converted = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record SI(string ShortPrefix, string LongPrefix, int Factor) {
|
||||
internal static readonly List<SI> All = [
|
||||
new SI("Q", "quetta", 30),
|
||||
new SI("R", "ronna", 27),
|
||||
new SI("Y", "yotta", 24),
|
||||
new SI("Z", "zetta", 21),
|
||||
new SI("E", "exa", 18),
|
||||
new SI("P", "peta", 15),
|
||||
new SI("T", "tera", 12),
|
||||
new SI("G", "giga", 9),
|
||||
new SI("M", "mega", 6),
|
||||
new SI("k", "kilo", 3),
|
||||
new SI("h", "hecto", 2),
|
||||
new SI("da", "deca", 1),
|
||||
new SI("d", "deci", -1),
|
||||
new SI("c", "centi", -2),
|
||||
new SI("m", "milli", -3),
|
||||
new SI("μ", "micro", -6),
|
||||
new SI("n", "nano", -9),
|
||||
new SI("p", "pico", -12),
|
||||
new SI("f", "femto", -15),
|
||||
new SI("a", "atto", -18),
|
||||
new SI("z", "zepto", -21),
|
||||
new SI("y", "yocto", -24),
|
||||
new SI("r", "ronto", -27),
|
||||
new SI("q", "quecto", -30)
|
||||
];
|
||||
}
|
||||
|
||||
internal sealed class Builder {
|
||||
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionToPrimaryUnit = new (ReferenceEqualityComparer.Instance);
|
||||
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit = new (ReferenceEqualityComparer.Instance);
|
||||
private readonly Unit primaryUnit;
|
||||
|
||||
public Builder(Unit primaryUnit) {
|
||||
this.primaryUnit = primaryUnit;
|
||||
AddUnit(primaryUnit, 1);
|
||||
}
|
||||
|
||||
public Builder AddUnit(Unit unit, Func<Number, Number> convertToPrimaryUnit, Func<Number, Number> convertFromPrimaryUnit) {
|
||||
unitToConversionToPrimaryUnit.Add(unit, convertToPrimaryUnit);
|
||||
unitToConversionFromPrimaryUnit.Add(unit, convertFromPrimaryUnit);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder AddUnit(Unit unit, Number amountInPrimaryUnit) {
|
||||
return AddUnit(unit, number => number * amountInPrimaryUnit, number => number / amountInPrimaryUnit);
|
||||
}
|
||||
|
||||
private void AddUnitSI(SI si, Func<SI, Unit> unitFactory, Func<int, int> factorModifier) {
|
||||
int factor = factorModifier(si.Factor);
|
||||
BigInteger powerOfTen = BigInteger.Pow(10, System.Math.Abs(factor));
|
||||
BigRational amountInPrimaryUnit = factor > 0 ? new BigRational(powerOfTen) : new BigRational(1, powerOfTen);
|
||||
AddUnit(unitFactory(si), amountInPrimaryUnit);
|
||||
}
|
||||
|
||||
public Builder AddSI(Func<SI, Unit> unitFactory, Func<int, int> factorModifier) {
|
||||
foreach (SI si in SI.All) {
|
||||
AddUnitSI(si, unitFactory, factorModifier);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder AddSI(Func<int, int> factorModifier) {
|
||||
Unit PrefixPrimaryUnit(SI si) {
|
||||
return new Unit(si.ShortPrefix + primaryUnit.ShortName, [..primaryUnit.LongNames.Select(longName => si.LongPrefix + longName)]);
|
||||
}
|
||||
|
||||
foreach (SI si in SI.All) {
|
||||
AddUnitSI(si, PrefixPrimaryUnit, factorModifier);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder AddSI() {
|
||||
return AddSI(static factor => factor);
|
||||
}
|
||||
|
||||
public UnitUniverse Build() {
|
||||
return new UnitUniverse(
|
||||
primaryUnit,
|
||||
unitToConversionToPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance),
|
||||
unitToConversionFromPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
33
Calculator/Math/UnitUniverses.cs
Normal file
33
Calculator/Math/UnitUniverses.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Calculator.Math;
|
||||
|
||||
sealed class UnitUniverses {
|
||||
private readonly FrozenDictionary<Unit, UnitUniverse> unitToUniverse;
|
||||
private readonly FrozenDictionary<string, Unit> nameToUnit;
|
||||
|
||||
internal UnitUniverses(params UnitUniverse[] universes) {
|
||||
Dictionary<Unit, UnitUniverse> unitToUniverseBuilder = new (ReferenceEqualityComparer.Instance);
|
||||
Dictionary<string, Unit> nameToUnitBuilder = new ();
|
||||
|
||||
foreach (UnitUniverse universe in universes) {
|
||||
foreach (Unit unit in universe.AllUnits) {
|
||||
unitToUniverseBuilder.Add(unit, universe);
|
||||
unit.AssignNamesTo(nameToUnitBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
unitToUniverse = unitToUniverseBuilder.ToFrozenDictionary(ReferenceEqualityComparer.Instance);
|
||||
nameToUnit = nameToUnitBuilder.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
public bool TryGetUnit(string name, [NotNullWhen(true)] out Unit? unit) {
|
||||
return nameToUnit.TryGetValue(name, out unit);
|
||||
}
|
||||
|
||||
public bool TryGetUniverse(Unit unit, [NotNullWhen(true)] out UnitUniverse? universe) {
|
||||
return unitToUniverse.TryGetValue(unit, out universe);
|
||||
}
|
||||
}
|
156
Calculator/Math/Units.cs
Normal file
156
Calculator/Math/Units.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Calculator.Parser;
|
||||
using ExtendedNumerics;
|
||||
|
||||
namespace Calculator.Math;
|
||||
|
||||
[SuppressMessage("ReSharper", "ConvertToConstant.Local")]
|
||||
[SuppressMessage("ReSharper", "HeapView.ObjectAllocation")]
|
||||
static class Units {
|
||||
public static UnitUniverse Time { get; } = TimeUniverse().Build();
|
||||
public static UnitUniverse Length { get; } = LengthUniverse().Build();
|
||||
public static UnitUniverse Mass { get; } = MassUniverse().Build();
|
||||
public static UnitUniverse Area { get; } = AreaUniverse().Build();
|
||||
public static UnitUniverse Volume { get; } = VolumeUniverse().Build();
|
||||
public static UnitUniverse Angle { get; } = AngleUniverse().Build();
|
||||
public static UnitUniverse Temperature { get; } = TemperatureUniverse().Build();
|
||||
public static UnitUniverse InformationEntropy { get; } = InformationEntropyUniverse().Build();
|
||||
|
||||
public static UnitUniverses All { get; } = new (Time, Length, Mass, Area, Volume, Angle, Temperature, InformationEntropy);
|
||||
|
||||
private static UnitUniverse.Builder TimeUniverse() {
|
||||
var minute = 60;
|
||||
var hour = minute * 60;
|
||||
var day = hour * 24;
|
||||
var week = day * 7;
|
||||
|
||||
return new UnitUniverse.Builder(new Unit("s", Pluralize("second")))
|
||||
.AddUnit(new Unit("min", Pluralize("minute")), minute)
|
||||
.AddUnit(new Unit("h", Pluralize("hour")), hour)
|
||||
.AddUnit(new Unit("d", Pluralize("day")), day)
|
||||
.AddUnit(new Unit("wk", Pluralize("week")), week);
|
||||
}
|
||||
|
||||
private static UnitUniverse.Builder LengthUniverse() {
|
||||
var inch = Parse("0", "0254");
|
||||
var foot = inch * 12;
|
||||
var yard = foot * 3;
|
||||
var furlong = yard * 220;
|
||||
var mile = yard * 1760;
|
||||
var nauticalMile = 1_852;
|
||||
var lightYear = 9_460_730_472_580_800;
|
||||
|
||||
return new UnitUniverse.Builder(new Unit("m", Pluralize("meter", "metre")))
|
||||
.AddSI()
|
||||
.AddUnit(new Unit("in", [ "inch", "inches", "\"" ]), inch)
|
||||
.AddUnit(new Unit("ft", [ "foot", "feet", "'" ]), foot)
|
||||
.AddUnit(new Unit("yd", Pluralize("yard")), yard)
|
||||
.AddUnit(new Unit("fur", Pluralize("furlong")), furlong)
|
||||
.AddUnit(new Unit("mi", Pluralize("mile")), mile)
|
||||
.AddUnit(new Unit("nmi", Pluralize("nautical mile")), nauticalMile)
|
||||
.AddUnit(new Unit("ly", Pluralize("light-year", "light year")), lightYear);
|
||||
}
|
||||
|
||||
private static UnitUniverse.Builder MassUniverse() {
|
||||
var pound = Parse("453", "59237");
|
||||
var stone = pound * 14;
|
||||
var ounce = pound / 16;
|
||||
var dram = ounce / 16;
|
||||
|
||||
return new UnitUniverse.Builder(new Unit("g", Pluralize("gram")))
|
||||
.AddSI()
|
||||
.AddUnit(new Unit("lb", [ "lbs", "pound", "pounds" ]), pound)
|
||||
.AddUnit(new Unit("st", Pluralize("stone")), stone)
|
||||
.AddUnit(new Unit("oz", Pluralize("ounce")), ounce)
|
||||
.AddUnit(new Unit("dr", Pluralize("dram")), dram);
|
||||
}
|
||||
|
||||
private static UnitUniverse.Builder AreaUniverse() {
|
||||
static Unit SquareMeter(string shortPrefix, string longPrefix) {
|
||||
return new Unit(shortPrefix + "m2", [
|
||||
$"square {shortPrefix}m",
|
||||
$"{shortPrefix}m square",
|
||||
$"{shortPrefix}m squared",
|
||||
$"{longPrefix}meter squared",
|
||||
$"{longPrefix}meters squared",
|
||||
$"{longPrefix}metre squared",
|
||||
$"{longPrefix}metres squared",
|
||||
..Pluralize($"square {longPrefix}meter", $"square {longPrefix}metre")
|
||||
]);
|
||||
}
|
||||
|
||||
return new UnitUniverse.Builder(SquareMeter(string.Empty, string.Empty))
|
||||
.AddSI(static si => SquareMeter(si.ShortPrefix, si.LongPrefix), static factor => factor * 2)
|
||||
.AddUnit(new Unit("a", Pluralize("are")), 100)
|
||||
.AddUnit(new Unit("ha", Pluralize("hectare")), 10_000);
|
||||
}
|
||||
|
||||
private static UnitUniverse.Builder VolumeUniverse() {
|
||||
static Unit CubicMeter(string shortPrefix, string longPrefix) {
|
||||
return new Unit(shortPrefix + "m3", [
|
||||
$"cubic {shortPrefix}m",
|
||||
$"{shortPrefix}m cubed",
|
||||
$"{longPrefix}meter cubed",
|
||||
$"{longPrefix}meters cubed",
|
||||
$"{longPrefix}metre cubed",
|
||||
$"{longPrefix}metres cubed",
|
||||
..Pluralize($"cubic {longPrefix}meter", $"cubic {longPrefix}metre")
|
||||
]);
|
||||
}
|
||||
|
||||
return new UnitUniverse.Builder(new Unit("l", Pluralize("litre", "liter")))
|
||||
.AddSI()
|
||||
.AddUnit(CubicMeter(string.Empty, string.Empty), 1000)
|
||||
.AddSI(static si => CubicMeter(si.ShortPrefix, si.LongPrefix), static factor => (factor * 3) + 3);
|
||||
}
|
||||
|
||||
private static UnitUniverse.Builder AngleUniverse() {
|
||||
return new UnitUniverse.Builder(new Unit("deg", [ "°", "degree", "degrees" ]))
|
||||
.AddUnit(new Unit("rad", Pluralize("radian")), new Number.Decimal((decimal) System.Math.PI / 180M))
|
||||
.AddUnit(new Unit("grad", Pluralize("gradian", "grade", "gon")), Ratio(9, 10));
|
||||
}
|
||||
|
||||
private static BigRational KelvinOffset { get; } = Parse("273", "15");
|
||||
|
||||
private static UnitUniverse.Builder TemperatureUniverse() {
|
||||
return new UnitUniverse.Builder(new Unit("°C", [ "C", "Celsius", "celsius" ]))
|
||||
.AddUnit(new Unit("°F", [ "F", "Fahrenheit", "fahrenheit" ]), static f => (f - 32) * Ratio(5, 9), static c => c * Ratio(9, 5) + 32)
|
||||
.AddUnit(new Unit("K", [ "Kelvin", "kelvin" ]), static k => k - KelvinOffset, static c => c + KelvinOffset);
|
||||
}
|
||||
|
||||
private static UnitUniverse.Builder InformationEntropyUniverse() {
|
||||
var bit = Ratio(1, 8);
|
||||
var nibble = bit * 4;
|
||||
|
||||
return new UnitUniverse.Builder(new Unit("B", Pluralize("byte")))
|
||||
.AddSI()
|
||||
.AddUnit(new Unit("b", Pluralize("bit")), bit)
|
||||
.AddUnit(new Unit("nibbles", [ "nibble" ]), nibble)
|
||||
.AddUnit(new Unit("KiB", Pluralize("kibibyte")), Pow(1024, 1))
|
||||
.AddUnit(new Unit("MiB", Pluralize("mebibyte")), Pow(1024, 2))
|
||||
.AddUnit(new Unit("GiB", Pluralize("gibibyte")), Pow(1024, 3))
|
||||
.AddUnit(new Unit("TiB", Pluralize("tebibyte")), Pow(1024, 4))
|
||||
.AddUnit(new Unit("PiB", Pluralize("pebibyte")), Pow(1024, 5))
|
||||
.AddUnit(new Unit("EiB", Pluralize("exbibyte")), Pow(1024, 6))
|
||||
.AddUnit(new Unit("ZiB", Pluralize("zebibyte")), Pow(1024, 7))
|
||||
.AddUnit(new Unit("YiB", Pluralize("yobibyte")), Pow(1024, 8));
|
||||
}
|
||||
|
||||
private static BigRational Parse(string integerPart, string fractionalPart) {
|
||||
return Tokenizer.ParseNumber(integerPart, fractionalPart);
|
||||
}
|
||||
|
||||
private static BigRational Ratio(long numerator, long denominator) {
|
||||
return new BigRational(numerator, denominator);
|
||||
}
|
||||
|
||||
private static BigRational Pow(int value, int exponent) {
|
||||
return BigRational.Pow(value, exponent);
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> Pluralize(params string[] names) {
|
||||
return [..names.SelectMany(static name => new [] { name, name + "s" })];
|
||||
}
|
||||
}
|
57
Calculator/Parser/Expression.cs
Normal file
57
Calculator/Parser/Expression.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Calculator.Math;
|
||||
|
||||
namespace Calculator.Parser;
|
||||
|
||||
public abstract record Expression {
|
||||
private Expression() {}
|
||||
|
||||
public abstract T Accept<T>(ExpressionVisitor<T> visitor);
|
||||
|
||||
public sealed record Number(Token.Number NumberToken) : Expression {
|
||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||
return visitor.VisitNumber(this);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record NumbersWithUnits(ImmutableArray<(Token.Number, Unit)> NumberTokensWithUnits) : Expression {
|
||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||
return visitor.VisitNumbersWithUnits(this);
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return nameof(NumbersWithUnits) + " { " + string.Join(", ", NumberTokensWithUnits.Select(static (number, unit) => number + " " + unit)) + " }";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record Grouping(Expression Expression) : Expression {
|
||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||
return visitor.VisitGrouping(this);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record Unary(Token.Simple Operator, Expression Right) : Expression {
|
||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||
return visitor.VisitUnary(this);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record Binary(Expression Left, Token.Simple Operator, Expression Right) : Expression {
|
||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||
return visitor.VisitBinary(this);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record UnitAssignment(Expression Left, Unit Unit) : Expression {
|
||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||
return visitor.VisitUnitAssignment(this);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record UnitConversion(Expression Left, Unit Unit) : Expression {
|
||||
public override T Accept<T>(ExpressionVisitor<T> visitor) {
|
||||
return visitor.VisitUnitConversion(this);
|
||||
}
|
||||
}
|
||||
}
|
17
Calculator/Parser/ExpressionVisitor.cs
Normal file
17
Calculator/Parser/ExpressionVisitor.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Calculator.Parser;
|
||||
|
||||
public interface ExpressionVisitor<T> {
|
||||
T VisitNumber(Expression.Number number);
|
||||
|
||||
T VisitNumbersWithUnits(Expression.NumbersWithUnits numbersWithUnits);
|
||||
|
||||
T VisitGrouping(Expression.Grouping grouping);
|
||||
|
||||
T VisitUnary(Expression.Unary unary);
|
||||
|
||||
T VisitBinary(Expression.Binary binary);
|
||||
|
||||
T VisitUnitAssignment(Expression.UnitAssignment unitAssignment);
|
||||
|
||||
T VisitUnitConversion(Expression.UnitConversion unitConversion);
|
||||
}
|
5
Calculator/Parser/ParseException.cs
Normal file
5
Calculator/Parser/ParseException.cs
Normal file
@ -0,0 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace Calculator.Parser;
|
||||
|
||||
sealed class ParseException(string message) : Exception(message);
|
216
Calculator/Parser/Parser.cs
Normal file
216
Calculator/Parser/Parser.cs
Normal file
@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Calculator.Math;
|
||||
|
||||
namespace Calculator.Parser;
|
||||
|
||||
public sealed class Parser(ImmutableArray<Token> tokens) {
|
||||
private int current = 0;
|
||||
|
||||
private bool IsEOF => current >= tokens.Length;
|
||||
|
||||
private static readonly ImmutableArray<SimpleTokenType> PLUS_MINUS = [
|
||||
SimpleTokenType.PLUS,
|
||||
SimpleTokenType.MINUS
|
||||
];
|
||||
|
||||
private static readonly ImmutableArray<SimpleTokenType> STAR_SLASH_PERCENT = [
|
||||
SimpleTokenType.STAR,
|
||||
SimpleTokenType.SLASH,
|
||||
SimpleTokenType.PERCENT
|
||||
];
|
||||
|
||||
private static readonly ImmutableArray<SimpleTokenType> CARET = [
|
||||
SimpleTokenType.CARET
|
||||
];
|
||||
|
||||
private bool Match(SimpleTokenType expectedTokenType, [NotNullWhen(true)] out Token.Simple? token) {
|
||||
return Match(simpleToken => simpleToken.Type == expectedTokenType, out token);
|
||||
}
|
||||
|
||||
private bool Match(ImmutableArray<SimpleTokenType> expectedTokenTypes, [NotNullWhen(true)] out Token.Simple? token) {
|
||||
return Match(simpleToken => expectedTokenTypes.Contains(simpleToken.Type), out token);
|
||||
}
|
||||
|
||||
private bool Match<T>([NotNullWhen(true)] out T? token) where T : Token {
|
||||
return Match(static _ => true, out token);
|
||||
}
|
||||
|
||||
private bool Match<T>(Predicate<T> predicate, [NotNullWhen(true)] out T? token) where T : Token {
|
||||
if (!IsEOF && tokens[current] is T t && predicate(t)) {
|
||||
current++;
|
||||
token = t;
|
||||
return true;
|
||||
}
|
||||
|
||||
token = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Expression Parse() {
|
||||
Expression term = Term();
|
||||
|
||||
if (!IsEOF) {
|
||||
throw new ParseException("Incomplete expression");
|
||||
}
|
||||
|
||||
return term;
|
||||
}
|
||||
|
||||
private Expression Term() {
|
||||
return Binary(Factor, PLUS_MINUS);
|
||||
}
|
||||
|
||||
private Expression Factor() {
|
||||
return Binary(Exponentiation, STAR_SLASH_PERCENT);
|
||||
}
|
||||
|
||||
private Expression Exponentiation() {
|
||||
return Binary(Conversion, CARET);
|
||||
}
|
||||
|
||||
private Expression Binary(Func<Expression> term, ImmutableArray<SimpleTokenType> expectedTokenTypes) {
|
||||
Expression left = term();
|
||||
|
||||
while (Match(expectedTokenTypes, out Token.Simple? op)) {
|
||||
Expression right = term();
|
||||
left = new Expression.Binary(left, op, right);
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression Conversion() {
|
||||
Expression left = Unary();
|
||||
|
||||
while (MatchUnitConversionOperator()) {
|
||||
if (!MatchUnit(out Unit? unit)) {
|
||||
throw new ParseException("Expected a unit literal");
|
||||
}
|
||||
|
||||
left = new Expression.UnitConversion(left, unit);
|
||||
}
|
||||
|
||||
return left;
|
||||
}
|
||||
|
||||
private Expression Unary() {
|
||||
if (Match(PLUS_MINUS, out Token.Simple? op)) {
|
||||
Expression right = Unary();
|
||||
return new Expression.Unary(op, right);
|
||||
}
|
||||
else {
|
||||
return Primary();
|
||||
}
|
||||
}
|
||||
|
||||
private Expression Primary() {
|
||||
if (Match(LiteralPredicate, out Token? literal)) {
|
||||
if (literal is not Token.Number number) {
|
||||
throw new ParseException("Expected a number literal");
|
||||
}
|
||||
|
||||
Expression expression;
|
||||
|
||||
if (!MatchUnit(out Unit? unit)) {
|
||||
expression = new Expression.Number(number);
|
||||
}
|
||||
else {
|
||||
var numbersWithUnits = ImmutableArray.CreateBuilder<(Token.Number, Unit)>();
|
||||
numbersWithUnits.Add((number, unit));
|
||||
|
||||
while (MatchNumberWithUnit(out var numberWithUnit)) {
|
||||
numbersWithUnits.Add(numberWithUnit.Value);
|
||||
}
|
||||
|
||||
expression = new Expression.NumbersWithUnits(numbersWithUnits.ToImmutable());
|
||||
}
|
||||
|
||||
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
|
||||
expression = new Expression.Binary(expression, new Token.Simple(SimpleTokenType.STAR), InsideParentheses());
|
||||
}
|
||||
|
||||
return expression;
|
||||
}
|
||||
|
||||
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
|
||||
return new Expression.Grouping(InsideParentheses());
|
||||
}
|
||||
|
||||
throw new ParseException("Unexpected token type: " + tokens[current]);
|
||||
}
|
||||
|
||||
private static bool LiteralPredicate(Token token) {
|
||||
return token is Token.Text or Token.Number;
|
||||
}
|
||||
|
||||
private Expression InsideParentheses() {
|
||||
Expression term = Term();
|
||||
|
||||
if (!Match(SimpleTokenType.RIGHT_PARENTHESIS, out _)) {
|
||||
throw new ParseException("Expected ')' after expression.");
|
||||
}
|
||||
|
||||
int position = current;
|
||||
|
||||
if (MatchUnitConversionOperator()) {
|
||||
if (MatchUnit(out Unit? toUnit)) {
|
||||
return new Expression.UnitConversion(term, toUnit);
|
||||
}
|
||||
else {
|
||||
current = position;
|
||||
}
|
||||
}
|
||||
|
||||
if (MatchUnit(out Unit? unit)) {
|
||||
return new Expression.UnitAssignment(term, unit);
|
||||
}
|
||||
else {
|
||||
return term;
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchNumberWithUnit([NotNullWhen(true)] out (Token.Number, Unit)? numberWithUnit) {
|
||||
if (!Match(out Token.Number? number)) {
|
||||
numberWithUnit = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MatchUnit(out Unit? unit)) {
|
||||
throw new ParseException("Expected a unit literal");
|
||||
}
|
||||
|
||||
numberWithUnit = (number, unit);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MatchUnit([NotNullWhen(true)] out Unit? unit) {
|
||||
int position = current;
|
||||
|
||||
List<string> words = [];
|
||||
|
||||
while (Match(out Token.Text? text)) {
|
||||
words.Add(text.Value);
|
||||
}
|
||||
|
||||
for (int i = words.Count; i > 0; i--) {
|
||||
string unitName = string.Join(' ', words.Take(i));
|
||||
|
||||
if (Units.All.TryGetUnit(unitName, out unit)) {
|
||||
current = position + i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
current = position;
|
||||
unit = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool MatchUnitConversionOperator() {
|
||||
return Match<Token.Text>(static text => text.Value is "to" or "in", out _);
|
||||
}
|
||||
}
|
13
Calculator/Parser/SimpleTokenType.cs
Normal file
13
Calculator/Parser/SimpleTokenType.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Calculator.Parser;
|
||||
|
||||
public enum SimpleTokenType {
|
||||
PLUS,
|
||||
MINUS,
|
||||
STAR,
|
||||
SLASH,
|
||||
PERCENT,
|
||||
CARET,
|
||||
|
||||
LEFT_PARENTHESIS,
|
||||
RIGHT_PARENTHESIS
|
||||
}
|
38
Calculator/Parser/Token.cs
Normal file
38
Calculator/Parser/Token.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Calculator.Parser;
|
||||
|
||||
public abstract record Token {
|
||||
private Token() {}
|
||||
|
||||
public sealed record Simple(SimpleTokenType Type) : Token {
|
||||
#pragma warning disable CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
|
||||
public override string ToString() {
|
||||
return Type switch {
|
||||
SimpleTokenType.PLUS => "+",
|
||||
SimpleTokenType.MINUS => "-",
|
||||
SimpleTokenType.STAR => "*",
|
||||
SimpleTokenType.SLASH => "/",
|
||||
SimpleTokenType.PERCENT => "%",
|
||||
SimpleTokenType.CARET => "^",
|
||||
SimpleTokenType.LEFT_PARENTHESIS => "(",
|
||||
SimpleTokenType.RIGHT_PARENTHESIS => ")",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
|
||||
}
|
||||
|
||||
public sealed record Text(string Value) : Token {
|
||||
public override string ToString() {
|
||||
return Value;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record Number(Math.Number Value) : Token {
|
||||
public override string ToString() {
|
||||
return Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
7
Calculator/Parser/TokenizationException.cs
Normal file
7
Calculator/Parser/TokenizationException.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using System;
|
||||
|
||||
namespace Calculator.Parser;
|
||||
|
||||
sealed class TokenizationException(string message, char character) : Exception(message) {
|
||||
public char Character { get; } = character;
|
||||
}
|
148
Calculator/Parser/Tokenizer.cs
Normal file
148
Calculator/Parser/Tokenizer.cs
Normal file
@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using ExtendedNumerics;
|
||||
|
||||
namespace Calculator.Parser;
|
||||
|
||||
public sealed class Tokenizer(string input) {
|
||||
private int position = 0;
|
||||
|
||||
private bool IsEOF => position >= input.Length;
|
||||
|
||||
private char Advance() {
|
||||
return input[position++];
|
||||
}
|
||||
|
||||
private bool Match(char c) {
|
||||
return Match(found => found == c, out _);
|
||||
}
|
||||
|
||||
private bool Match(Predicate<char> predicate, out char c) {
|
||||
if (IsEOF) {
|
||||
c = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
c = input[position];
|
||||
|
||||
if (!predicate(c)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
position++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void MatchWhile(StringBuilder result, Predicate<char> predicate) {
|
||||
while (Match(predicate, out char c)) {
|
||||
result.Append(c);
|
||||
}
|
||||
}
|
||||
|
||||
private string MatchWhile(Predicate<char> predicate) {
|
||||
var result = new StringBuilder();
|
||||
MatchWhile(result, predicate);
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private string MatchRest(char firstChar, Predicate<char> predicate) {
|
||||
var result = new StringBuilder();
|
||||
result.Append(firstChar);
|
||||
MatchWhile(result, predicate);
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
public ImmutableArray<Token> Scan() {
|
||||
ImmutableArray<Token>.Builder tokens = ImmutableArray.CreateBuilder<Token>();
|
||||
|
||||
void AddToken(Token token) {
|
||||
tokens.Add(token);
|
||||
}
|
||||
|
||||
void AddSimpleToken(SimpleTokenType tokenType) {
|
||||
AddToken(new Token.Simple(tokenType));
|
||||
}
|
||||
|
||||
while (!IsEOF) {
|
||||
char c = Advance();
|
||||
switch (c) {
|
||||
case ' ':
|
||||
// Ignore whitespace.
|
||||
break;
|
||||
|
||||
case '+':
|
||||
AddSimpleToken(SimpleTokenType.PLUS);
|
||||
break;
|
||||
|
||||
case '-':
|
||||
AddSimpleToken(SimpleTokenType.MINUS);
|
||||
break;
|
||||
|
||||
case '*':
|
||||
AddSimpleToken(SimpleTokenType.STAR);
|
||||
break;
|
||||
|
||||
case '/':
|
||||
AddSimpleToken(SimpleTokenType.SLASH);
|
||||
break;
|
||||
|
||||
case '%':
|
||||
AddSimpleToken(SimpleTokenType.PERCENT);
|
||||
break;
|
||||
|
||||
case '^':
|
||||
AddSimpleToken(SimpleTokenType.CARET);
|
||||
break;
|
||||
|
||||
case '(':
|
||||
AddSimpleToken(SimpleTokenType.LEFT_PARENTHESIS);
|
||||
break;
|
||||
|
||||
case ')':
|
||||
AddSimpleToken(SimpleTokenType.RIGHT_PARENTHESIS);
|
||||
break;
|
||||
|
||||
case '"' or '\'':
|
||||
AddToken(new Token.Text(c.ToString()));
|
||||
break;
|
||||
|
||||
case '°':
|
||||
case {} when char.IsLetter(c):
|
||||
AddToken(new Token.Text(MatchRest(c, char.IsLetterOrDigit)));
|
||||
break;
|
||||
|
||||
case {} when char.IsAsciiDigit(c):
|
||||
string integerPart = MatchRest(c, char.IsAsciiDigit);
|
||||
|
||||
if (Match('.')) {
|
||||
string fractionalPart = MatchWhile(char.IsAsciiDigit);
|
||||
AddToken(new Token.Number(ParseNumber(integerPart, fractionalPart)));
|
||||
}
|
||||
else {
|
||||
AddToken(new Token.Number(ParseNumber(integerPart)));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new TokenizationException("Unexpected character: " + c, c);
|
||||
}
|
||||
}
|
||||
|
||||
return tokens.ToImmutable();
|
||||
}
|
||||
|
||||
internal static BigRational ParseNumber(string integerPart, string? fractionalPart = null) {
|
||||
if (fractionalPart == null) {
|
||||
return new BigRational(BigInteger.Parse(integerPart, NumberStyles.Integer, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else {
|
||||
BigInteger numerator = BigInteger.Parse(integerPart + fractionalPart, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
BigInteger denominator = BigInteger.Pow(10, fractionalPart.Length);
|
||||
return new BigRational(numerator, denominator);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppMeme", "AppMeme\AppMeme.
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AppSys", "AppWindows\AppSys.csproj", "{E71AFA58-A144-4170-AF7B-05730C04CF59}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calculator", "Calculator\Calculator.csproj", "{C4B1529F-943C-4C30-A40A-4A4C248FC92F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -47,6 +49,10 @@ Global
|
||||
{E71AFA58-A144-4170-AF7B-05730C04CF59}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E71AFA58-A144-4170-AF7B-05730C04CF59}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E71AFA58-A144-4170-AF7B-05730C04CF59}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C4B1529F-943C-4C30-A40A-4A4C248FC92F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C4B1529F-943C-4C30-A40A-4A4C248FC92F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C4B1529F-943C-4C30-A40A-4A4C248FC92F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C4B1529F-943C-4C30-A40A-4A4C248FC92F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
Loading…
Reference in New Issue
Block a user