Skip to content

Commit

Permalink
feat: parse and handle shorthand and infinite bound numeric intervals (
Browse files Browse the repository at this point in the history
  • Loading branch information
Seddryck authored Jan 6, 2024
1 parent 3b52f94 commit 505eccb
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 12 deletions.
70 changes: 70 additions & 0 deletions Expressif.Testing/Parsers/IntervalTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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", ']')]
Expand Down
16 changes: 16 additions & 0 deletions Expressif.Testing/Predicates/Numeric/IntervalTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<decimal>(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>(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<decimal>(12, decimal.MaxValue, IntervalType.Closed, IntervalType.Closed)).Evaluate(value), Is.EqualTo(expected));
}
36 changes: 36 additions & 0 deletions Expressif.Testing/Values/IntervalBuilderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Interval<decimal>>());
});

[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<Interval<decimal>>());
});
Assert.Multiple(() =>
{
Assert.That(((Interval<decimal>)interval).LowerBoundIntervalType, Is.EqualTo(IntervalType.Closed));
Assert.That(((Interval<decimal>)interval).LowerBound, Is.EqualTo(0));
Assert.That(((Interval<decimal>)interval).UpperBound, Is.EqualTo(decimal.MaxValue));
Assert.That(((Interval<decimal>)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<Interval<decimal>>());
});
Assert.Multiple(() =>
{
Assert.That(((Interval<decimal>)interval).LowerBoundIntervalType, Is.EqualTo(IntervalType.Closed));
Assert.That(((Interval<decimal>)interval).LowerBound, Is.EqualTo(decimal.MinValue));
Assert.That(((Interval<decimal>)interval).UpperBound, Is.EqualTo(45));
Assert.That(((Interval<decimal>)interval).UpperBoundIntervalType, Is.EqualTo(IntervalType.Closed));
});
}
}
58 changes: 50 additions & 8 deletions Expressif/Parsers/Interval.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,14 +10,7 @@ namespace Expressif.Parsers;

public class Interval
{
public static readonly Parser<Interval> 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; }
Expand All @@ -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<Interval> 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<Interval> 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<Interval> 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<Interval> 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<Interval> Parser =
Classic.Or(ZeroBasedShorthand).Or(NonZeroBasedShorthand).Or(ZeroBasedLonghand);
}
6 changes: 4 additions & 2 deletions Expressif/Predicates/Text/Matching.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -42,10 +43,11 @@ public MatchesNumeric(Func<string> 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";
}

/// <summary>
Expand Down
18 changes: 16 additions & 2 deletions Expressif/Values/Casters/NumericCaster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
{
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;
}
}

0 comments on commit 505eccb

Please sign in to comment.