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

Compare commits

...

8 Commits

23 changed files with 514 additions and 372 deletions

View File

@@ -1,40 +1,41 @@
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;
}
public NumberWithUnit VisitGrouping(Expression.Grouping grouping) {
return Evaluate(grouping.Expression);
}
public NumberWithUnit VisitUnary(Expression.Unary unary) {
(Token.Simple op, Expression right) = unary;
return op.Type switch {
SimpleTokenType.PLUS => +Evaluate(right),
SimpleTokenType.MINUS => -Evaluate(right),
_ => throw new CalculatorException("Unsupported unary operator: " + op.Type)
};
}
public NumberWithUnit VisitBinary(Expression.Binary binary) {
(Expression left, Token.Simple op, Expression right) = binary;
return op.Type switch {
SimpleTokenType.PLUS => Evaluate(left) + Evaluate(right),
SimpleTokenType.MINUS => Evaluate(left) - Evaluate(right),
@@ -45,26 +46,26 @@ public sealed class CalculatorExpressionVisitor : ExpressionVisitor<NumberWithUn
_ => throw new CalculatorException("Unsupported binary operator: " + op.Type)
};
}
public NumberWithUnit VisitUnitAssignment(Expression.UnitAssignment unitAssignment) {
(Expression left, Unit right) = unitAssignment;
NumberWithUnit number = Evaluate(left);
if (number.Unit is null) {
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);
}
}
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) {
return expression.Accept(this);
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Numerics;
using ExtendedNumerics;
@@ -16,65 +17,117 @@ 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);
}
/// <summary>
/// Represents an integer number with arbitrary precision.
/// </summary>
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 = rationalExponent.GetImproperFraction();
if (fractionExponent.Denominator == 1 && fractionExponent.Numerator >= 0) {
Fraction fractionExponent = Fraction.ReduceToProperFraction(rationalExponent.GetImproperFraction());
if (fractionExponent.Numerator >= 0 && fractionExponent.Denominator == 1) {
try {
return new Rational(BigRational.Pow(Value, fractionExponent.Numerator));
} catch (OverflowException) {}
}
if (fractionExponent.Numerator == 1 && fractionExponent.Denominator > 1) {
Number result = PowAsDecimal(exponent);
BigRational assumedPerfectPowerRoot = new BigRational(decimal.Floor(result.AsDecimal));
BigRational assumedPerfectPower = BigRational.Pow(assumedPerfectPowerRoot, fractionExponent.Denominator);
return assumedPerfectPower == Value ? assumedPerfectPowerRoot : result;
}
}
return PowAsDecimal(exponent);
Number PowAsDecimal(Number number) {
return new Decimal(AsDecimal).Pow(number);
}
return new Decimal(AsDecimal).Pow(exponent);
}
public override string ToString(IFormatProvider? formatProvider) {
Fraction fraction = Value.GetImproperFraction();
return fraction.Denominator == 1 ? fraction.Numerator.ToString(formatProvider) : AsDecimal.ToString(formatProvider);
BigRational value = Fraction.ReduceToProperFraction(Value.GetImproperFraction());
string wholePartStr = value.WholePart.ToString(formatProvider);
if (value.FractionalPart.IsZero) {
return wholePartStr;
}
decimal fractionalPart = (decimal) value.FractionalPart;
if (fractionalPart == 0) {
return wholePartStr + ".0...";
}
string decimalPartStr = fractionalPart.ToString(formatProvider);
int decimalSeparatorIndex = decimalPartStr.TakeWhile(char.IsAsciiDigit).Count();
return wholePartStr + decimalPartStr[decimalSeparatorIndex..];
}
public override int CompareTo(Number? other) {
if (other is Rational rational) {
return Value.CompareTo(rational.Value);
}
else {
return base.CompareTo(other);
}
}
}
/// <summary>
/// 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 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;
return new Decimal(System.Math.Pow(doubleValue, doubleExponent));
}
public override string ToString(IFormatProvider? formatProvider) {
return Value.ToString(formatProvider);
}
}
public static implicit operator Number(BigRational value) {
return new Rational(value);
}
public static implicit operator Number(int value) {
return new Rational(value);
}
@@ -85,65 +138,65 @@ public abstract record Number : IAdditionOperators<Number, Number, Number>,
public static Rational AdditiveIdentity => new (BigInteger.Zero);
public static Rational MultiplicativeIdentity => new (BigInteger.One);
public static Number Add(Number left, Number right) {
return left + right;
}
public static Number Subtract(Number left, Number right) {
return left - right;
}
public static Number Multiply(Number left, Number right) {
return left * right;
}
public static Number Divide(Number left, Number right) {
return left / right;
}
public static Number Remainder(Number left, Number right) {
return left % right;
}
public static Number Pow(Number left, Number right) {
return left.Pow(right);
}
public static Number operator +(Number value) {
return value;
}
public static Number operator -(Number value) {
return Operate(value, BigRational.Negate, decimal.Negate);
}
public static Number operator +(Number left, Number right) {
return Operate(left, right, BigRational.Add, decimal.Add);
}
public static Number operator -(Number left, Number right) {
return Operate(left, right, BigRational.Subtract, decimal.Subtract);
}
public static Number operator *(Number left, Number right) {
return Operate(left, right, BigRational.Multiply, decimal.Multiply);
}
public static Number operator /(Number left, Number right) {
return Operate(left, right, BigRational.Divide, decimal.Divide);
}
public static Number operator %(Number left, Number right) {
return Operate(left, right, BigRational.Mod, decimal.Remainder);
}
private static Number Operate(Number value, Func<BigRational, BigRational> rationalOperation, Func<decimal, decimal> decimalOperation) {
return value is Rational rational
? new Rational(rationalOperation(rational.Value))
: new Decimal(decimalOperation(value.AsDecimal));
}
private static Number Operate(Number left, Number right, Func<BigRational, BigRational, BigRational> rationalOperation, Func<decimal, decimal, decimal> decimalOperation) {
return left is Rational leftRational && right is Rational rightRational
? new Rational(rationalOperation(leftRational.Value, rightRational.Value))

View File

@@ -1,57 +1,109 @@
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() {
return ToString(CultureInfo.InvariantCulture);
}
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 {
@@ -59,13 +111,13 @@ 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 {
@@ -73,36 +125,61 @@ public readonly record struct NumberWithUnit(Number Number, Unit? Unit) : IAddit
}
});
}
public static NumberWithUnit operator *(NumberWithUnit left, NumberWithUnit right) {
return OperateWithoutUnits(left, right, Number.Multiply, "Cannot multiply");
}
public static NumberWithUnit operator /(NumberWithUnit left, NumberWithUnit right) {
return OperateWithoutUnits(left, right, Number.Divide, "Cannot divide");
}
public static NumberWithUnit operator %(NumberWithUnit left, NumberWithUnit right) {
return OperateWithoutUnits(left, right, Number.Remainder, "Cannot modulo");
}
public NumberWithUnit Pow(NumberWithUnit exponent) {
return OperateWithoutUnits(this, exponent, Number.Pow, "Cannot exponentiate");
}
private static NumberWithUnit Operate(NumberWithUnit left, NumberWithUnit right, Func<Number, Number, Number> withoutUnitsOperation, Func<Number, Unit, Number, Unit, NumberWithUnit> withUnitsOperation) {
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;
@@ -29,89 +34,96 @@ sealed class UnitUniverse(
return false;
}
}
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) {
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);
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);
}
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(
name,
unitToConversionToPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance),
unitToConversionFromPrimaryUnit.ToFrozenDictionary(ReferenceEqualityComparer.Instance)
);

View File

@@ -9,7 +9,7 @@ sealed class UnitUniverses {
private readonly FrozenDictionary<Unit, UnitUniverse> unitToUniverse;
public WordLookupTrieNode UnitLookupByWords { get; }
internal UnitUniverses(params UnitUniverse[] universes) {
Dictionary<Unit, UnitUniverse> unitToUniverseBuilder = new (ReferenceEqualityComparer.Instance);
WordLookupTrieNode.Builder unitLookupByWordsBuilder = new ();
@@ -20,7 +20,7 @@ sealed class UnitUniverses {
unitLookupByWordsBuilder.Add(unit);
}
}
unitToUniverse = unitToUniverseBuilder.ToFrozenDictionary(ReferenceEqualityComparer.Instance);
UnitLookupByWords = unitLookupByWordsBuilder.Build();
}
@@ -58,14 +58,14 @@ sealed class UnitUniverses {
private void Add(string name, Unit unit) {
Node node = root;
string[] words = name.Split(' ');
foreach (string word in words.AsSpan(..^1)) {
node = node.Child(word);
}
node.Children.Add(words[^1], Node.Create(unit));
}
public WordLookupTrieNode Build() {
return Build(root);
}

View File

@@ -17,22 +17,22 @@ static class Units {
public static UnitUniverse Angle { get; } = AngleUniverse().Build();
public static UnitUniverse Temperature { get; } = TemperatureUniverse().Build();
public static UnitUniverse InformationEntropy { get; } = InformationEntropyUniverse().Build();
public static UnitUniverses All { get; } = new (Time, Length, Mass, Area, Volume, Angle, Temperature, InformationEntropy);
private static UnitUniverse.Builder TimeUniverse() {
var minute = 60;
var hour = minute * 60;
var day = hour * 24;
var week = day * 7;
return new UnitUniverse.Builder(new Unit("s", Pluralize("second")))
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)
.AddUnit(new Unit("wk", Pluralize("week")), week);
}
private static UnitUniverse.Builder LengthUniverse() {
var inch = Parse("0", "0254");
var foot = inch * 12;
@@ -41,8 +41,8 @@ static class Units {
var mile = yard * 1760;
var nauticalMile = 1_852;
var lightYear = 9_460_730_472_580_800;
return new UnitUniverse.Builder(new Unit("m", Pluralize("meter", "metre")))
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)
@@ -52,21 +52,21 @@ static class Units {
.AddUnit(new Unit("nmi", Pluralize("nautical mile")), nauticalMile)
.AddUnit(new Unit("ly", Pluralize("light-year", "light year")), lightYear);
}
private static UnitUniverse.Builder MassUniverse() {
var pound = Parse("453", "59237");
var stone = pound * 14;
var ounce = pound / 16;
var dram = ounce / 16;
return new UnitUniverse.Builder(new Unit("g", Pluralize("gram")))
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)
.AddUnit(new Unit("oz", Pluralize("ounce")), ounce)
.AddUnit(new Unit("dr", Pluralize("dram")), dram);
}
private static UnitUniverse.Builder AreaUniverse() {
static Unit SquareMeter(string shortPrefix, string longPrefix) {
return new Unit(shortPrefix + "m2", [
@@ -80,13 +80,13 @@ static class Units {
..Pluralize($"square {longPrefix}meter", $"square {longPrefix}metre")
]);
}
return new UnitUniverse.Builder(SquareMeter(string.Empty, string.Empty))
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() {
static Unit CubicMeter(string shortPrefix, string longPrefix) {
return new Unit(shortPrefix + "m3", [
@@ -99,57 +99,57 @@ static class Units {
..Pluralize($"cubic {longPrefix}meter", $"cubic {longPrefix}metre")
]);
}
return new UnitUniverse.Builder(new Unit("l", Pluralize("litre", "liter")))
return new UnitUniverse.Builder("Volume", new Unit("l", Pluralize("litre", "liter")))
.AddSI()
.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) {
return Tokenizer.ParseNumber(integerPart, fractionalPart);
}
private static BigRational Ratio(long numerator, long denominator) {
return new BigRational(numerator, denominator);
}
private static BigRational Pow(int value, int exponent) {
return BigRational.Pow(value, exponent);
}
private static ImmutableArray<string> Pluralize(params string[] names) {
return [..names.SelectMany(static name => new [] { name, name + "s" })];
}

View File

@@ -6,9 +6,9 @@ namespace Calculator.Parser;
public abstract record Expression {
private Expression() {}
public abstract T Accept<T>(ExpressionVisitor<T> visitor);
public sealed record Number(Token.Number NumberToken) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitNumber(this);
@@ -19,7 +19,7 @@ public abstract record Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitNumbersWithUnits(this);
}
public override string ToString() {
return nameof(NumbersWithUnits) + " { " + string.Join(", ", NumberTokensWithUnits.Select(static (number, unit) => number + " " + unit)) + " }";
}
@@ -30,26 +30,26 @@ public abstract record Expression {
return visitor.VisitGrouping(this);
}
}
public sealed record Unary(Token.Simple Operator, Expression Right) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitUnary(this);
}
}
public sealed record Binary(Expression Left, Token.Simple Operator, Expression Right) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitBinary(this);
}
}
public sealed record UnitAssignment(Expression Left, Unit Unit) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitUnitAssignment(this);
}
}
public sealed record UnitConversion(Expression Left, Unit Unit) : Expression {
public sealed record UnitConversion(Expression Left, ImmutableArray<Unit> Units) : Expression {
public override T Accept<T>(ExpressionVisitor<T> visitor) {
return visitor.VisitUnitConversion(this);
}

View File

@@ -2,16 +2,16 @@
public interface ExpressionVisitor<T> {
T VisitNumber(Expression.Number number);
T VisitNumbersWithUnits(Expression.NumbersWithUnits numbersWithUnits);
T VisitGrouping(Expression.Grouping grouping);
T VisitUnary(Expression.Unary unary);
T VisitBinary(Expression.Binary binary);
T VisitUnitAssignment(Expression.UnitAssignment unitAssignment);
T VisitUnitConversion(Expression.UnitConversion unitConversion);
}

View File

@@ -7,94 +7,89 @@ 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
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) {
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 (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");
}
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);
return Binary(Unary, CARET);
}
private Expression Binary(Func<Expression> term, ImmutableArray<SimpleTokenType> expectedTokenTypes) {
Expression left = term();
while (Match(expectedTokenTypes, out Token.Simple? op)) {
Expression right = term();
left = new Expression.Binary(left, op, right);
}
return left;
}
private Expression Conversion() {
Expression left = Unary();
while (MatchUnitConversionOperator()) {
if (!MatchUnit(out Unit? unit)) {
throw new ParseException("Expected a unit literal");
}
left = new Expression.UnitConversion(left, unit);
}
return left;
}
private Expression Unary() {
if (Match(PLUS_MINUS, out Token.Simple? op)) {
Expression right = Unary();
@@ -104,65 +99,54 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
return Primary();
}
}
private Expression Primary() {
if (Match(LiteralPredicate, out Token? literal)) {
if (literal is not Token.Number number) {
throw new ParseException("Expected a number literal");
}
Expression expression;
if (!MatchUnit(out Unit? unit)) {
expression = new Expression.Number(number);
}
else {
var numbersWithUnits = ImmutableArray.CreateBuilder<(Token.Number, Unit)>();
numbersWithUnits.Add((number, unit));
while (MatchNumberWithUnit(out var numberWithUnit)) {
numbersWithUnits.Add(numberWithUnit.Value);
}
expression = new Expression.NumbersWithUnits(numbersWithUnits.ToImmutable());
}
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
expression = new Expression.Binary(expression, new Token.Simple(SimpleTokenType.STAR), InsideParentheses());
}
return expression;
}
if (Match(SimpleTokenType.LEFT_PARENTHESIS, out _)) {
return new Expression.Grouping(InsideParentheses());
}
throw new ParseException("Unexpected token type: " + tokens[current]);
}
private static bool LiteralPredicate(Token token) {
return token is Token.Text or Token.Number;
}
private Expression InsideParentheses() {
Expression term = Term();
if (!Match(SimpleTokenType.RIGHT_PARENTHESIS, out _)) {
throw new ParseException("Expected ')' after expression.");
}
int position = current;
if (MatchUnitConversionOperator()) {
if (MatchUnit(out Unit? toUnit)) {
return new Expression.UnitConversion(term, toUnit);
}
else {
current = position;
}
}
if (MatchUnit(out Unit? unit)) {
return new Expression.UnitAssignment(term, unit);
}
@@ -170,33 +154,50 @@ public sealed class Parser(ImmutableArray<Token> tokens) {
return term;
}
}
private bool MatchNumberWithUnit([NotNullWhen(true)] out (Token.Number, Unit)? numberWithUnit) {
if (!Match(out Token.Number? number)) {
numberWithUnit = null;
return false;
}
if (!MatchUnit(out Unit? unit)) {
throw new ParseException("Expected a unit literal");
}
numberWithUnit = (number, unit);
return true;
}
private bool 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;
UnitUniverses.WordLookupTrieNode node = Units.All.UnitLookupByWords;
// ReSharper disable once AccessToModifiedClosure
while (Match(token => node.Children.ContainsKey(token.Value), out Token.Text? text)) {
node = node.Children[text.Value];
}
unit = node.Unit;
if (unit != null) {
return true;
}
@@ -205,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

@@ -7,7 +7,7 @@ public enum SimpleTokenType {
SLASH,
PERCENT,
CARET,
LEFT_PARENTHESIS,
RIGHT_PARENTHESIS
}

View File

@@ -5,7 +5,7 @@ namespace Calculator.Parser;
public abstract record Token {
private Token() {}
public sealed record Simple(SimpleTokenType Type) : Token {
#pragma warning disable CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
public override string ToString() {
@@ -23,13 +23,13 @@ public abstract record Token {
}
#pragma warning restore CS8524 // The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value.
}
public sealed record Text(string Value) : Token {
public override string ToString() {
return Value;
}
}
public sealed record Number(Math.Number Value) : Token {
public override string ToString() {
return Value.ToString(CultureInfo.InvariantCulture);

View File

@@ -9,114 +9,114 @@ namespace Calculator.Parser;
public sealed class Tokenizer(string input) {
private int position = 0;
private bool IsEOF => position >= input.Length;
private char Advance() {
return input[position++];
}
private bool Match(char c) {
return Match(found => found == c, out _);
}
private bool Match(Predicate<char> predicate, out char c) {
if (IsEOF) {
c = default;
return false;
}
c = input[position];
if (!predicate(c)) {
return false;
}
position++;
return true;
}
private void MatchWhile(StringBuilder result, Predicate<char> predicate) {
while (Match(predicate, out char c)) {
result.Append(c);
}
}
private string MatchWhile(Predicate<char> predicate) {
var result = new StringBuilder();
MatchWhile(result, predicate);
return result.ToString();
}
private string MatchRest(char firstChar, Predicate<char> predicate) {
var result = new StringBuilder();
result.Append(firstChar);
MatchWhile(result, predicate);
return result.ToString();
}
public ImmutableArray<Token> Scan() {
ImmutableArray<Token>.Builder tokens = ImmutableArray.CreateBuilder<Token>();
void AddToken(Token token) {
tokens.Add(token);
}
void AddSimpleToken(SimpleTokenType tokenType) {
AddToken(new Token.Simple(tokenType));
}
while (!IsEOF) {
char c = Advance();
switch (c) {
case ' ':
// Ignore whitespace.
break;
case '+':
AddSimpleToken(SimpleTokenType.PLUS);
break;
case '-':
AddSimpleToken(SimpleTokenType.MINUS);
break;
case '*':
AddSimpleToken(SimpleTokenType.STAR);
break;
case '/':
AddSimpleToken(SimpleTokenType.SLASH);
break;
case '%':
AddSimpleToken(SimpleTokenType.PERCENT);
break;
case '^':
AddSimpleToken(SimpleTokenType.CARET);
break;
case '(':
AddSimpleToken(SimpleTokenType.LEFT_PARENTHESIS);
break;
case ')':
AddSimpleToken(SimpleTokenType.RIGHT_PARENTHESIS);
break;
case '"' or '\'':
AddToken(new Token.Text(c.ToString()));
break;
case '°':
case {} when char.IsLetter(c):
AddToken(new Token.Text(MatchRest(c, char.IsLetterOrDigit)));
break;
case {} when char.IsAsciiDigit(c):
string integerPart = MatchRest(c, char.IsAsciiDigit);
if (Match('.')) {
string fractionalPart = MatchWhile(char.IsAsciiDigit);
AddToken(new Token.Number(ParseNumber(integerPart, fractionalPart)));
@@ -124,17 +124,17 @@ public sealed class Tokenizer(string input) {
else {
AddToken(new Token.Number(ParseNumber(integerPart)));
}
break;
default:
throw new TokenizationException("Unexpected character: " + c, c);
}
}
return tokens.ToImmutable();
}
internal static BigRational ParseNumber(string integerPart, string? fractionalPart = null) {
if (fractionalPart == null) {
return new BigRational(BigInteger.Parse(integerPart, NumberStyles.Integer, CultureInfo.InvariantCulture));

View File

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

View File

@@ -11,7 +11,7 @@ sealed class CalculatorApp : IApp {
ImmutableArray<Token> tokens = new Tokenizer(command).Scan();
Expression expression = new Parser(tokens).Parse();
NumberWithUnit result = expression.Accept(new CalculatorExpressionVisitor());
output = result.ToString();
return true;
}

View File

@@ -14,11 +14,11 @@ sealed class KillProcessApp : IApp {
}
int succeeded = 0, failed = 0;
foreach (string processName in args[1..]) {
try {
Process[] processes = Process.GetProcessesByName(processName.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase) ? processName[..^4] : processName);
foreach (Process process in processes) {
try {
process.Kill();
@@ -26,21 +26,21 @@ sealed class KillProcessApp : IApp {
} catch {
++failed;
}
process.Close();
}
} catch {
++failed;
}
}
var build = new StringBuilder();
build.Append("Killed ").Append(succeeded).Append(" process").Append(succeeded == 1 ? "" : "es");
if (failed > 0) {
build.Append(", failed ").Append(failed);
}
output = build.Append('.').ToString();
return true;
}

View File

@@ -10,7 +10,7 @@ sealed class MemeApp : IApp {
{ "flip", @"(╯°□°)╯︵ ┻━┻" },
{ "tableflip", @"(╯°□°)╯︵ ┻━┻" }
};
public bool TryRun(string command, [NotNullWhen(true)] out string? output) {
return Map.TryGetValue(command, out output);
}

View File

@@ -5,19 +5,19 @@ namespace Query.Command;
sealed class CommandHistory {
private readonly List<string> queries = [];
private readonly List<string> results = [];
public IList<string> Queries => queries;
public IList<string> Results => results;
public void AddQuery(string text) {
queries.Add(text);
}
public void AddResult(string text) {
results.Add(text);
}
public void Clear() {
queries.Clear();
results.Clear();

View File

@@ -10,7 +10,7 @@ sealed class CommandProcessor {
new KillProcessApp(),
new CalculatorApp()
];
public string Run(string command) {
try {
foreach (IApp app in apps) {
@@ -18,7 +18,7 @@ sealed class CommandProcessor {
return output;
}
}
return "Unknown command.";
} catch (Exception e) {
throw new CommandException(e.Message, e);

View File

@@ -6,57 +6,57 @@ namespace Query.Form;
sealed class KeyboardHook {
public event EventHandler? Triggered;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly NativeMethods.HookProc keyboardHookDelegate;
private IntPtr keyboardHook;
public KeyboardHook() {
keyboardHookDelegate = KeyboardHookProc;
}
public void StartHook() {
if (keyboardHook != IntPtr.Zero) {
NativeMethods.UnhookWindowsHookEx(keyboardHook);
}
keyboardHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_KEYBOARD_LL, keyboardHookDelegate, IntPtr.Zero, 0);
}
public void StopHook() {
if (keyboardHook != IntPtr.Zero) {
NativeMethods.UnhookWindowsHookEx(keyboardHook);
keyboardHook = IntPtr.Zero;
}
}
private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam) {
if (wParam == NativeMethods.WM_KEYDOWN) {
Keys key = (Keys) Marshal.ReadInt32(lParam);
if (key is Keys.LWin or Keys.RWin && Control.ModifierKeys.HasFlag(Keys.Control)) {
Triggered?.Invoke(this, EventArgs.Empty);
return NativeMethods.HookHandled;
}
}
return NativeMethods.CallNextHookEx(keyboardHook, nCode, wParam, lParam);
}
private static class NativeMethods {
public const int WH_KEYBOARD_LL = 13;
public const int WM_KEYDOWN = 0x0100;
public static readonly IntPtr HookHandled = new (-1);
public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);
[DllImport("user32.dll")]
public static extern bool UnhookWindowsHookEx(IntPtr idHook);
[DllImport("user32.dll")]
public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);
}

View File

@@ -64,7 +64,7 @@ sealed partial class MainForm : System.Windows.Forms.Form {
private bool isMoving = false;
private void FixLocation() {
if (!isMoving && Screen.FromControl(this) is {} screen) {
if (!isMoving && WindowState != FormWindowState.Minimized && Screen.FromControl(this) is {} screen) {
Rectangle screenRect = screen.WorkingArea;
isMoving = true;
Location = new Point(screenRect.X + screenRect.Width - Width, screenRect.Y + screenRect.Height - Height);
@@ -104,8 +104,10 @@ sealed partial class MainForm : System.Windows.Forms.Form {
}
private void focusTimer_Tick(object? sender, EventArgs e) {
WindowState = FormWindowState.Minimized;
Show();
Activate();
WindowState = FormWindowState.Normal;
queryBox.Focus();
focusTimer.Stop();

View File

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

View File

@@ -11,21 +11,21 @@ sealed partial class QueryHistoryLog : UserControl {
Information,
Error
}
private static readonly Dictionary<EntryType, Color> EntryColorMap = new () {
{ EntryType.UserInput, Color.FromArgb(160, 160, 160) },
{ EntryType.CommandResult, Color.FromArgb(240, 240, 240) },
{ EntryType.Information, Color.FromArgb(160, 255, 140) },
{ EntryType.Error, Color.FromArgb(255, 40, 40) }
};
public QueryHistoryLog() {
InitializeComponent();
}
public void AddEntry(string text, EntryType type) {
int width = container.Width - SystemInformation.VerticalScrollBarWidth;
Label label = new Label {
AutoSize = true,
Font = container.Font,
@@ -35,11 +35,11 @@ sealed partial class QueryHistoryLog : UserControl {
MaximumSize = new Size(width, 0),
Width = width
};
container.Controls.Add(label);
container.AutoScrollPosition = new Point(0, container.VerticalScroll.Maximum);
}
public void ClearEntries() {
container.Controls.Clear();
}

View File

@@ -8,84 +8,84 @@ namespace Query.Form;
sealed partial class QueryTextBox : UserControl {
public event EventHandler<CommandEventArgs>? CommandRan;
private CommandHistory history = null!;
private Action<string> log = null!;
public QueryTextBox() {
InitializeComponent();
}
public void Setup(CommandHistory historyObj, Action<string> logFunc) {
history = historyObj;
log = logFunc;
}
private void OnCommandRan() {
CommandRan?.Invoke(this, new CommandEventArgs(tb.Text));
}
private sealed class CustomTextBox : TextBox {
private string lastInputStr = string.Empty;
private int lastInputPos = 0;
private bool doResetHistoryMemory;
private bool lastArrowShift;
private int historyOffset;
public CustomTextBox() {
TextChanged += CustomTextBox_TextChanged;
}
protected override void OnKeyDown(KeyEventArgs e) {
QueryTextBox input = (QueryTextBox) Parent!;
CommandHistory history = input.history;
Keys key = e.KeyCode;
bool handled = false;
switch (key) {
case Keys.Enter:
if (Text != string.Empty) {
input.OnCommandRan();
Text = string.Empty;
doResetHistoryMemory = true;
handled = true;
}
break;
case Keys.Up:
if (lastArrowShift != e.Shift) {
lastArrowShift = e.Shift;
historyOffset = 0;
}
--historyOffset;
if (InsertFromHistory(e.Shift ? history.Results : history.Queries)) {
++historyOffset;
}
handled = true;
break;
case Keys.Down:
if (lastArrowShift != e.Shift) {
lastArrowShift = e.Shift;
historyOffset = 0;
}
++historyOffset;
if (InsertFromHistory(e.Shift ? history.Results : history.Queries)) {
--historyOffset;
}
handled = true;
break;
case Keys.C:
if (e.Modifiers == Keys.Control) {
if (SelectionLength == 0 && history.Results.Count > 0) {
@@ -94,47 +94,47 @@ sealed partial class QueryTextBox : UserControl {
handled = true;
}
}
break;
}
if (!handled && key != Keys.ControlKey && key != Keys.ShiftKey && key != Keys.Menu) {
doResetHistoryMemory = true;
}
e.Handled = e.SuppressKeyPress = handled;
base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyEventArgs e) {
base.OnKeyUp(e);
if (doResetHistoryMemory) {
doResetHistoryMemory = false;
ResetHistoryMemory();
}
}
private void CustomTextBox_TextChanged(object? sender, EventArgs e) {
ResetHistoryMemory();
}
// Management
private void ResetHistoryMemory() {
lastInputStr = Text;
lastInputPos = SelectionStart;
historyOffset = 0;
}
private bool InsertFromHistory(IList<string> collection) {
if (collection.Count == 0) {
return true;
}
int index = collection.Count + historyOffset;
bool wasClamped = false;
if (index < 0) {
index = 0;
wasClamped = true;
@@ -143,13 +143,13 @@ sealed partial class QueryTextBox : UserControl {
index = collection.Count - 1;
wasClamped = true;
}
TextChanged -= CustomTextBox_TextChanged;
Text = lastInputStr.Insert(lastInputPos, collection[index]);
SelectionStart = lastInputPos + collection[index].Length;
SelectionLength = 0;
TextChanged += CustomTextBox_TextChanged;
return wasClamped;
}