diff --git a/Expressif.Testing/Parsers/IntervalTest.cs b/Expressif.Testing/Parsers/IntervalTest.cs index 0fbb966..7cdd207 100644 --- a/Expressif.Testing/Parsers/IntervalTest.cs +++ b/Expressif.Testing/Parsers/IntervalTest.cs @@ -29,6 +29,76 @@ public void Parse_IntervalDecimal_Valid(string value, char lowerBoundIntervalTyp }); } + [Test] + [TestCase("[25;+INF]", '[', "25", "+INF", ']')] + [TestCase("[-INF;40[", '[', "-INF", "40", '[')] + public void Parse_IntervalInfinite_Valid(string value, char lowerBoundIntervalType, string lowerBound, string upperBound, char upperBoundIntervalType) + { + var interval = Interval.Parser.End().Parse(value); + Assert.That(interval, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(interval.LowerBoundType, Is.EqualTo(lowerBoundIntervalType)); + Assert.That(interval.UpperBoundType, Is.EqualTo(upperBoundIntervalType)); + Assert.That(interval.LowerBound, Is.EqualTo(lowerBound)); + Assert.That(interval.UpperBound, Is.EqualTo(upperBound)); + }); + } + + [Test] + [TestCase("(+)", ']', "0", "+INF", ']')] + [TestCase("(-)", '[', "-INF", "0", '[')] + [TestCase("(0+)", '[', "0", "+INF", ']')] + [TestCase("(0-)", '[', "-INF", "0", ']')] + public void Parse_IntervalZeroBasedShorthand_Valid(string value, char lowerBoundIntervalType, string lowerBound, string upperBound, char upperBoundIntervalType) + { + var interval = Interval.Parser.End().Parse(value); + Assert.That(interval, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(interval.LowerBoundType, Is.EqualTo(lowerBoundIntervalType)); + Assert.That(interval.UpperBoundType, Is.EqualTo(upperBoundIntervalType)); + Assert.That(interval.LowerBound, Is.EqualTo(lowerBound)); + Assert.That(interval.UpperBound, Is.EqualTo(upperBound)); + }); + } + + [Test] + [TestCase("(absolutely-positive)", ']', "0", "+INF", ']')] + [TestCase("(absolutely-negative)", '[', "-INF", "0", '[')] + [TestCase("(positive)", '[', "0", "+INF", ']')] + [TestCase("(negative)", '[', "-INF", "0", ']')] + public void Parse_IntervalZeroBasedLonghand_Valid(string value, char lowerBoundIntervalType, string lowerBound, string upperBound, char upperBoundIntervalType) + { + var interval = Interval.Parser.End().Parse(value); + Assert.That(interval, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(interval.LowerBoundType, Is.EqualTo(lowerBoundIntervalType)); + Assert.That(interval.UpperBoundType, Is.EqualTo(upperBoundIntervalType)); + Assert.That(interval.LowerBound, Is.EqualTo(lowerBound)); + Assert.That(interval.UpperBound, Is.EqualTo(upperBound)); + }); + } + + [Test] + [TestCase("(>40)", ']', "40", "+INF", ']')] + [TestCase("(<40)", '[', "-INF", "40", '[')] + [TestCase("(>=40)", '[', "40", "+INF", ']')] + [TestCase("(<=40)", '[', "-INF", "40", ']')] + public void Parse_IntervalNonZeroBasedShorthand_Valid(string value, char lowerBoundIntervalType, string lowerBound, string upperBound, char upperBoundIntervalType) + { + var interval = Interval.Parser.End().Parse(value); + Assert.That(interval, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(interval.LowerBoundType, Is.EqualTo(lowerBoundIntervalType)); + Assert.That(interval.UpperBoundType, Is.EqualTo(upperBoundIntervalType)); + Assert.That(interval.LowerBound, Is.EqualTo(lowerBound)); + Assert.That(interval.UpperBound, Is.EqualTo(upperBound)); + }); + } + [Test] [TestCase("[2022-10-01;2022-12-01]", '[', "2022-10-01", "2022-12-01", ']')] [TestCase("]2022-10-01;2022-12-01]", ']', "2022-10-01", "2022-12-01", ']')] diff --git a/Expressif.Testing/Predicates/Numeric/IntervalTest.cs b/Expressif.Testing/Predicates/Numeric/IntervalTest.cs index 2770fe2..cfe2ef4 100644 --- a/Expressif.Testing/Predicates/Numeric/IntervalTest.cs +++ b/Expressif.Testing/Predicates/Numeric/IntervalTest.cs @@ -17,4 +17,20 @@ public class IntervalTest [TestCase(null, false)] public void WithinInterval_Numeric_Success(object? value, bool expected) => Assert.That(new WithinInterval(() => new Interval(1,12,IntervalType.Open,IntervalType.Closed)).Evaluate(value), Is.EqualTo(expected)); + + [Test] + [TestCase(10, true)] + [TestCase(1, true)] + [TestCase(12, true)] + [TestCase(null, false)] + public void WithinNegativeInfiniteInterval_Numeric_Success(object? value, bool expected) + => Assert.That(new WithinInterval(() => new Interval(decimal.MinValue, 12, IntervalType.Closed, IntervalType.Closed)).Evaluate(value), Is.EqualTo(expected)); + + [Test] + [TestCase(16, true)] + [TestCase(1, false)] + [TestCase(12, true)] + [TestCase(null, false)] + public void WithinPositiveInfiniteInterval_Numeric_Success(object? value, bool expected) + => Assert.That(new WithinInterval(() => new Interval(12, decimal.MaxValue, IntervalType.Closed, IntervalType.Closed)).Evaluate(value), Is.EqualTo(expected)); } diff --git a/Expressif.Testing/Values/IntervalBuilderTest.cs b/Expressif.Testing/Values/IntervalBuilderTest.cs index 275663e..d82be02 100644 --- a/Expressif.Testing/Values/IntervalBuilderTest.cs +++ b/Expressif.Testing/Values/IntervalBuilderTest.cs @@ -46,4 +46,40 @@ public void CreateNumeric_FromString_valid(string value) Assert.That(() => new IntervalBuilder().Create(value), Is.Not.Null); Assert.That(() => new IntervalBuilder().Create(value), Is.TypeOf>()); }); + + [Test] + public void CreateNumericWithPositiveInfinite_FromString_valid() + { + var interval = new IntervalBuilder().Create("(0+)"); + Assert.Multiple(() => + { + Assert.That(interval, Is.Not.Null); + Assert.That(interval, Is.TypeOf>()); + }); + Assert.Multiple(() => + { + Assert.That(((Interval)interval).LowerBoundIntervalType, Is.EqualTo(IntervalType.Closed)); + Assert.That(((Interval)interval).LowerBound, Is.EqualTo(0)); + Assert.That(((Interval)interval).UpperBound, Is.EqualTo(decimal.MaxValue)); + Assert.That(((Interval)interval).UpperBoundIntervalType, Is.EqualTo(IntervalType.Closed)); + }); + } + + [Test] + public void CreateNumericWithNegativeInfinite_FromString_valid() + { + var interval = new IntervalBuilder().Create("[-INF;45]"); + Assert.Multiple(() => + { + Assert.That(interval, Is.Not.Null); + Assert.That(interval, Is.TypeOf>()); + }); + Assert.Multiple(() => + { + Assert.That(((Interval)interval).LowerBoundIntervalType, Is.EqualTo(IntervalType.Closed)); + Assert.That(((Interval)interval).LowerBound, Is.EqualTo(decimal.MinValue)); + Assert.That(((Interval)interval).UpperBound, Is.EqualTo(45)); + Assert.That(((Interval)interval).UpperBoundIntervalType, Is.EqualTo(IntervalType.Closed)); + }); + } } diff --git a/Expressif/Parsers/Interval.cs b/Expressif/Parsers/Interval.cs index 77340f3..e83f62c 100644 --- a/Expressif/Parsers/Interval.cs +++ b/Expressif/Parsers/Interval.cs @@ -1,6 +1,7 @@ using Sprache; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -9,14 +10,7 @@ namespace Expressif.Parsers; public class Interval { - public static readonly Parser Parser = - from lowerBoundType in Parse.Chars(']', '[').Token() - from lowerBound in Parse.Numeric.Or(Parse.Chars('.', '-', ':', ' ')).AtLeastOnce().Text() - from separator in Parse.Char(';') - from upperBound in Parse.Numeric.Or(Parse.Chars('.', '-', ':', ' ')).AtLeastOnce().Text() - from upperBoundType in Parse.Chars(']', '[').Token() - select new Interval(lowerBoundType, lowerBound, upperBound, upperBoundType); - + public string LowerBound { get; } public string UpperBound { get; } public char LowerBoundType { get; } @@ -26,4 +20,52 @@ public Interval(char lowerBoundType, string lowerBound, string upperBound, char { (LowerBoundType, LowerBound, UpperBound, UpperBoundType) = (lowerBoundType, lowerBound, upperBound, upperBoundType); } + + protected static readonly Parser Classic = + from lowerBoundType in Parse.Chars(']', '[').Token() + from lowerBound in Parse.IgnoreCase("-INF").Or(Parse.Numeric.Or(Parse.Chars('.', '-', ':', ' ')).AtLeastOnce()).Text() + from separator in Parse.Char(';') + from upperBound in Parse.IgnoreCase("+INF").Or(Parse.Numeric.Or(Parse.Chars('.', '-', ':', ' ')).AtLeastOnce()).Text() + from upperBoundType in Parse.Chars(']', '[').Token() + select new Interval(lowerBoundType, lowerBound, upperBound, upperBoundType); + + protected static readonly Parser ZeroBasedShorthand = + from lp in Parse.Chars('(').Token() + from zero in Parse.Chars('0').Token().Optional() + from sign in Parse.Char('+').Or(Parse.Char('-')) + from rp in Parse.Chars(')').Token() + select new Interval( + sign == '+' && !zero.IsDefined ? ']' : '[', + sign == '+' ? "0" : "-INF", + sign == '+' ? "+INF" : "0", + sign == '-' && !zero.IsDefined ? '[' : ']' + ); + + protected static readonly Parser ZeroBasedLonghand = + from lp in Parse.Chars('(').Token() + from absolutely in Parse.IgnoreCase("absolutely-").Optional() + from sign in Parse.IgnoreCase("positive").Return('+').Or(Parse.IgnoreCase("negative").Return('-')) + from rp in Parse.Chars(')').Token() + select new Interval( + sign == '+' && absolutely.IsDefined ? ']' : '[', + sign == '+' ? "0" : "-INF", + sign == '+' ? "+INF" : "0", + sign == '-' && absolutely.IsDefined ? '[' : ']' + ); + + protected static readonly Parser NonZeroBasedShorthand = + from lp in Parse.Chars('(').Token() + from sign in Parse.Char('>').Or(Parse.Char('<')) + from equal in Parse.Chars('=').Token().Optional() + from bound in Parse.Numeric.Or(Parse.Chars('.', '-', ':', ' ')).AtLeastOnce().Text() + from rp in Parse.Chars(')').Token() + select new Interval( + sign == '>' && !equal.IsDefined ? ']' : '[', + sign == '>' ? bound : "-INF", + sign == '>' ? "+INF" : bound, + sign == '<' && !equal.IsDefined ? '[' : ']' + ); + + public static readonly Parser Parser = + Classic.Or(ZeroBasedShorthand).Or(NonZeroBasedShorthand).Or(ZeroBasedLonghand); } diff --git a/Expressif/Predicates/Text/Matching.cs b/Expressif/Predicates/Text/Matching.cs index 6357958..3876f3d 100644 --- a/Expressif/Predicates/Text/Matching.cs +++ b/Expressif/Predicates/Text/Matching.cs @@ -1,6 +1,7 @@ using Expressif.Values; using Expressif.Values.Casters; using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -42,10 +43,11 @@ public MatchesNumeric(Func culture) : base(culture) { } protected override bool EvaluateUncasted(object value) - => TypeChecker.IsNumericType(value) || base.EvaluateUncasted(value); + => TypeChecker.IsNumericType(value) || base.EvaluateUncasted(value); protected override bool EvaluateBaseText(string value) - => decimal.TryParse(value, NumberStyles.Number & ~NumberStyles.AllowThousands, CultureInfo.NumberFormat, out var _); + => decimal.TryParse(value, NumberStyles.Number & ~NumberStyles.AllowThousands, CultureInfo.NumberFormat, out var _) + || value.Trim() == "+INF" || value.Trim() == "-INF"; } /// diff --git a/Expressif/Values/Casters/NumericCaster.cs b/Expressif/Values/Casters/NumericCaster.cs index 4d38529..4c9f7bb 100644 --- a/Expressif/Values/Casters/NumericCaster.cs +++ b/Expressif/Values/Casters/NumericCaster.cs @@ -55,5 +55,19 @@ public virtual decimal Cast(object obj) : throw new InvalidCastException($"Cannot cast an object of type '{obj.GetType().FullName}' to virtual type Numeric. The type Numeric can only be casted from the underlying numeric types (int, float, ...), Boolean and String. The expect string format can include decimal point, thousand separators, sign symbol and white spaces."); public override bool TryParse(string text, [NotNullWhen(true)] out decimal value) - => decimal.TryParse(text, Style, Format, out value); -} \ No newline at end of file + { + if (decimal.TryParse(text, Style, Format, out value)) + return true; + if (string.Equals(text, "-INF", StringComparison.OrdinalIgnoreCase)) + { + value = decimal.MinValue; + return true; + } + if (string.Equals(text, "+INF", StringComparison.OrdinalIgnoreCase)) + { + value = decimal.MaxValue; + return true; + } + return false; + } +}