1
0
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:
chylex 2024-08-05 20:35:15 +02:00
parent 12c5418443
commit 146b68fa67
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
18 changed files with 1183 additions and 0 deletions

View 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>

View File

@ -0,0 +1,5 @@
using System;
namespace Calculator;
sealed class CalculatorException(string message) : Exception(message);

View 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
View 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));
}
}

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

View 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)
);
}
}
}

View 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
View 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" })];
}
}

View 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);
}
}
}

View 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);
}

View File

@ -0,0 +1,5 @@
using System;
namespace Calculator.Parser;
sealed class ParseException(string message) : Exception(message);

216
Calculator/Parser/Parser.cs Normal file
View 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 _);
}
}

View File

@ -0,0 +1,13 @@
namespace Calculator.Parser;
public enum SimpleTokenType {
PLUS,
MINUS,
STAR,
SLASH,
PERCENT,
CARET,
LEFT_PARENTHESIS,
RIGHT_PARENTHESIS
}

View 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);
}
}
}

View File

@ -0,0 +1,7 @@
using System;
namespace Calculator.Parser;
sealed class TokenizationException(string message, char character) : Exception(message) {
public char Character { get; } = character;
}

View 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);
}
}
}

View File

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