1
0
mirror of https://github.com/chylex/Query.git synced 2025-09-17 18:24:47 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
963f8da60c Release 2.2.0 2025-07-06 18:53:27 +02:00
b40fb751d0 Add support for displaying results with multiple units 2025-07-06 16:06:23 +02:00
8 changed files with 229 additions and 119 deletions

View File

@@ -1,18 +1,19 @@
using Calculator.Math;
using System.Collections.Immutable;
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);
return number.NumberToken.Value;
}
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);
result += new NumberWithUnit(number.Value, [ unit ]);
}
return result;
@@ -51,8 +52,8 @@ public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUn
NumberWithUnit number = Evaluate(left);
if (number.Unit is null) {
return number with { Unit = right };
if (number.PrimaryUnit is null) {
return new NumberWithUnit(number.Value, [ right ]);
}
else {
throw new CalculatorException("Expression already has a unit, cannot assign a new unit: " + right);
@@ -60,9 +61,9 @@ public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUn
}
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) {

View File

@@ -16,13 +16,21 @@ public abstract record Number : IAdditionOperators<Number, Number, Number>,
IUnaryPlusOperators<Number, Number>,
IUnaryNegationOperators<Number, Number>,
IAdditiveIdentity<Number, Number.Rational>,
IMultiplicativeIdentity<Number, Number.Rational> {
IMultiplicativeIdentity<Number, Number.Rational>,
IComparable<Number> {
protected abstract decimal AsDecimal { get; }
public abstract Number WholePart { get; }
public abstract bool IsZero { get; }
public abstract Number Pow(Number exponent);
public abstract string ToString(IFormatProvider? formatProvider);
public virtual int CompareTo(Number? other) {
return AsDecimal.CompareTo(other?.AsDecimal);
}
public sealed override string ToString() {
return ToString(CultureInfo.InvariantCulture);
}
@@ -33,6 +41,9 @@ public abstract record Number : IAdditionOperators<Number, Number, Number>,
public sealed record Rational(BigRational Value) : Number {
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) {
if (exponent is Rational { Value: {} rationalExponent }) {
Fraction fractionExponent = Fraction.ReduceToProperFraction(rationalExponent.GetImproperFraction());
@@ -64,6 +75,15 @@ public abstract record Number : IAdditionOperators<Number, Number, Number>,
Fraction fraction = Value.GetImproperFraction();
return fraction.Denominator == 1 ? fraction.Numerator.ToString(formatProvider) : AsDecimal.ToString(formatProvider);
}
public override int CompareTo(Number? other) {
if (other is Rational rational) {
return Value.CompareTo(rational.Value);
}
else {
return base.CompareTo(other);
}
}
}
/// <summary>
@@ -74,6 +94,9 @@ public abstract record Number : IAdditionOperators<Number, Number, Number>,
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) {
double doubleValue = (double) Value;
double doubleExponent = (double) exponent.AsDecimal;

View File

@@ -1,33 +1,85 @@
using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text;
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 };
public sealed class NumberWithUnit : 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 Number Value { get; }
public Unit? PrimaryUnit => Unit?.Primary;
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)) {
return new NumberWithUnit(converted, targetUnit);
else if (Unit.Universe.TryConvert(Value, Unit.Primary, targetUnits[0], out Number? converted)) {
return new NumberWithUnit(converted, targetUnits);
}
else {
throw new ArithmeticException("Cannot convert '" + Unit + "' to '" + targetUnit + "'");
throw new ArithmeticException("Cannot convert '" + Unit.Primary + "' to '" + targetUnits[0] + "'");
}
}
public string ToString(IFormatProvider? formatProvider) {
string number = Number.ToString(formatProvider);
return Unit == null ? number : number + " " + Unit;
if (Unit == null) {
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() {
@@ -35,23 +87,23 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
}
public static implicit operator NumberWithUnit(Number number) {
return new NumberWithUnit(number, null);
return new NumberWithUnit(number, unit: null);
}
public static NumberWithUnit operator +(NumberWithUnit value) {
return value with { Number = +value.Number };
return value.WithValue(+value.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) {
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);
}
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);
}
else {
@@ -62,10 +114,10 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
public static NumberWithUnit operator -(NumberWithUnit left, NumberWithUnit right) {
return Operate(left, right, Number.Subtract, static (leftNumber, leftUnit, rightNumber, rightUnit) => {
if (leftUnit == rightUnit) {
if (leftUnit.Primary == rightUnit.Primary) {
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);
}
else {
@@ -90,19 +142,44 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
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) {
return left with { Number = withoutUnitsOperation(left.Number, right.Number) };
return left.WithValue(withoutUnitsOperation(left.Value, right.Value));
}
else if (left.Unit is null) {
return right with { Number = withoutUnitsOperation(left.Number, right.Number) };
return right.WithValue(withoutUnitsOperation(left.Value, right.Value));
}
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) {
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);
}
}
}

View File

@@ -10,11 +10,16 @@ using ExtendedNumerics;
namespace Calculator.Math;
sealed class UnitUniverse(
string name,
FrozenDictionary<Unit, Func<Number, Number>> unitToConversionToPrimaryUnit,
FrozenDictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit
) {
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) {
if (fromUnit == toUnit) {
converted = value;
@@ -30,43 +35,49 @@ sealed class UnitUniverse(
}
}
public override string ToString() {
return name;
}
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)
new ("Q", "quetta", Factor: 30),
new ("R", "ronna", Factor: 27),
new ("Y", "yotta", Factor: 24),
new ("Z", "zetta", Factor: 21),
new ("E", "exa", Factor: 18),
new ("P", "peta", Factor: 15),
new ("T", "tera", Factor: 12),
new ("G", "giga", Factor: 9),
new ("M", "mega", Factor: 6),
new ("k", "kilo", Factor: 3),
new ("h", "hecto", Factor: 2),
new ("da", "deca", Factor: 1),
new ("d", "deci", Factor: -1),
new ("c", "centi", Factor: -2),
new ("m", "milli", Factor: -3),
new ("μ", "micro", Factor: -6),
new ("n", "nano", Factor: -9),
new ("p", "pico", Factor: -12),
new ("f", "femto", Factor: -15),
new ("a", "atto", Factor: -18),
new ("z", "zepto", Factor: -21),
new ("y", "yocto", Factor: -24),
new ("r", "ronto", Factor: -27),
new ("q", "quecto", Factor: -30),
];
}
internal sealed class Builder {
private readonly string name;
private readonly Unit primaryUnit;
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionToPrimaryUnit = new (ReferenceEqualityComparer.Instance);
private readonly Dictionary<Unit, Func<Number, Number>> unitToConversionFromPrimaryUnit = new (ReferenceEqualityComparer.Instance);
public Builder(Unit primaryUnit) {
public Builder(string name, Unit primaryUnit) {
this.name = name;
this.primaryUnit = primaryUnit;
AddUnit(primaryUnit, 1);
AddUnit(primaryUnit, amountInPrimaryUnit: 1);
}
public Builder AddUnit(Unit unit, Func<Number, Number> convertToPrimaryUnit, Func<Number, Number> convertFromPrimaryUnit) {
@@ -81,8 +92,8 @@ sealed class UnitUniverse(
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);
BigInteger powerOfTen = BigInteger.Pow(value: 10, System.Math.Abs(factor));
BigRational amountInPrimaryUnit = factor > 0 ? new BigRational(powerOfTen) : new BigRational(numerator: 1, powerOfTen);
AddUnit(unitFactory(si), amountInPrimaryUnit);
}
@@ -112,6 +123,7 @@ sealed class UnitUniverse(
public UnitUniverse Build() {
return new UnitUniverse(
name,
unitToConversionToPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance),
unitToConversionFromPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance)
);

View File

@@ -26,7 +26,7 @@ static class Units {
var day = hour * 24;
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("h", Pluralize("hour")), hour)
.AddUnit(new Unit("d", Pluralize("day")), day)
@@ -42,7 +42,7 @@ static class Units {
var nauticalMile = 1_852;
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()
.AddUnit(new Unit("in", [ "inch", "inches", "\"" ]), inch)
.AddUnit(new Unit("ft", [ "foot", "feet", "'" ]), foot)
@@ -59,7 +59,7 @@ static class Units {
var ounce = pound / 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()
.AddUnit(new Unit("lb", [ "lbs", "pound", "pounds" ]), pound)
.AddUnit(new Unit("st", Pluralize("stone")), stone)
@@ -81,10 +81,10 @@ static class Units {
]);
}
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)
.AddUnit(new Unit("a", Pluralize("are")), 100)
.AddUnit(new Unit("ha", Pluralize("hectare")), 10_000);
.AddUnit(new Unit("a", Pluralize("are")), amountInPrimaryUnit: 100)
.AddUnit(new Unit("ha", Pluralize("hectare")), amountInPrimaryUnit: 10_000);
}
private static UnitUniverse.Builder VolumeUniverse() {
@@ -100,42 +100,42 @@ static class Units {
]);
}
return new UnitUniverse.Builder(new Unit("l", Pluralize("litre", "liter")))
return new UnitUniverse.Builder("Volume", new Unit("l", Pluralize("litre", "liter")))
.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);
}
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("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 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)
return new UnitUniverse.Builder("Temperature", new Unit("°C", [ "C", "Celsius", "celsius" ]))
.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);
}
private static UnitUniverse.Builder InformationEntropyUniverse() {
var bit = Ratio(1, 8);
var bit = Ratio(numerator: 1, denominator: 8);
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()
.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));
.AddUnit(new Unit("KiB", Pluralize("kibibyte")), Pow(value: 1024, exponent: 1))
.AddUnit(new Unit("MiB", Pluralize("mebibyte")), Pow(value: 1024, exponent: 2))
.AddUnit(new Unit("GiB", Pluralize("gibibyte")), Pow(value: 1024, exponent: 3))
.AddUnit(new Unit("TiB", Pluralize("tebibyte")), Pow(value: 1024, exponent: 4))
.AddUnit(new Unit("PiB", Pluralize("pebibyte")), Pow(value: 1024, exponent: 5))
.AddUnit(new Unit("EiB", Pluralize("exbibyte")), Pow(value: 1024, exponent: 6))
.AddUnit(new Unit("ZiB", Pluralize("zebibyte")), Pow(value: 1024, exponent: 7))
.AddUnit(new Unit("YiB", Pluralize("yobibyte")), Pow(value: 1024, exponent: 8));
}
private static BigRational Parse(string integerPart, string fractionalPart) {

View File

@@ -49,7 +49,7 @@ public abstract record Expression {
}
}
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) {
return visitor.VisitUnitConversion(this);
}

View File

@@ -12,17 +12,17 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
private static readonly ImmutableArray<SimpleTokenType> PLUS_MINUS = [
SimpleTokenType.PLUS,
SimpleTokenType.MINUS
SimpleTokenType.MINUS,
];
private static readonly ImmutableArray<SimpleTokenType> STAR_SLASH_PERCENT = [
SimpleTokenType.STAR,
SimpleTokenType.SLASH,
SimpleTokenType.PERCENT
SimpleTokenType.PERCENT,
];
private static readonly ImmutableArray<SimpleTokenType> CARET = [
SimpleTokenType.CARET
SimpleTokenType.CARET,
];
private bool Match(SimpleTokenType expectedTokenType, [NotNullWhen(true)] out Token.Simple? token) {
@@ -51,6 +51,15 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
public Expression Parse() {
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) {
throw new ParseException("Incomplete expression");
}
@@ -67,7 +76,7 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
}
private Expression Exponentiation() {
return Binary(Conversion, CARET);
return Binary(Unary, CARET);
}
private Expression Binary(Func<Expression> term, ImmutableArray<SimpleTokenType> expectedTokenTypes) {
@@ -81,20 +90,6 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
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();
@@ -152,17 +147,6 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
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);
}
@@ -185,6 +169,23 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
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) {
int position = current;
@@ -205,8 +206,4 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
return false;
}
}
private bool MatchUnitConversionOperator() {
return Match<Token.Text>(static text => text.Value is "to" or "in", out _);
}
}

View File

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