Skip to content

Commit

Permalink
feat: better handling of null, empty, blank for text sorting and coun…
Browse files Browse the repository at this point in the history
…ting (#233)

* feat: handle null when using predicates for sorting text values

`null` is considered equivalent to `null`
`null` is not after or before any value

* feat: handle `null`, `blank`, and `empty` for TextCounting

returns `null` when the count is undefined
`null` and `empty` have a length of 0
`blank` has an undefined length
`null` and `empty` are never a substring of anything including `null` and `empty`
  • Loading branch information
Seddryck committed Dec 29, 2023
1 parent 6533e9c commit e504197
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 49 deletions.
24 changes: 18 additions & 6 deletions Expressif.Testing/Functions/Text/CountingFunctionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ public class CountingFunctionsTest
[TestCase(" foo ", 5)]
[TestCase("", 0)]
[TestCase("(null)", 0)]
[TestCase(null, 0)]
[TestCase("(empty)", 0)]
[TestCase("(blank)", -1)]
public void Length_Valid(object value, int expected)
[TestCase("(blank)", null)]
public void Length_Valid(object? value, int? expected)
=> Assert.That(new Length().Evaluate(value), Is.EqualTo(expected));

[Test]
Expand All @@ -25,9 +26,10 @@ public void Length_Valid(object value, int expected)
[TestCase("barfoobar", 5)]
[TestCase("FOOfoo", 4)]
[TestCase("(null)", 0)]
[TestCase(null, 0)]
[TestCase("(empty)", 0)]
[TestCase("(blank)", -1)]
public void CountDistinctChars_Valid(object value, int expected)
[TestCase("(blank)", null)]
public void CountDistinctChars_Valid(object? value, int? expected)
=> Assert.That(new CountDistinctChars().Evaluate(value), Is.EqualTo(expected));

[Test]
Expand All @@ -39,9 +41,19 @@ public void CountDistinctChars_Valid(object value, int expected)
[TestCase("barfoobarfoobar", "bar", 3)]
[TestCase("---*#*#*---", "*#*", 1)]
[TestCase("(null)", "foo", 0)]
[TestCase("(null)", "(null)", 0)]
[TestCase(null, "foo", 0)]
[TestCase(null, "(null)", 0)]
[TestCase("foo", "(null)", 0)]
[TestCase("foo", null, 0)]
[TestCase("(empty)", "foo", 0)]
[TestCase("(blank)", "foo", -1)]
public void CountSubstring_Valid(object value, string substring, int expected)
[TestCase("foo", "(empty)", 0)]
[TestCase("(empty)", "(empty)", 0)]
[TestCase("(blank)", "foo", 0)]
[TestCase("(blank)", " ", null)]
[TestCase("(blank)", "\t", null)]
[TestCase("(blank)", "\r\n", null)]
public void CountSubstring_Valid(object? value, string? substring, int? expected)
=> Assert.That(new CountSubstring(() => substring).Evaluate(value), Is.EqualTo(expected));

[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace Expressif.Testing.Predicates.Text;

public class EquivalentToTest
public class SortingTest
{
[TestCase("A", "A", true)]
[TestCase("A", "B", false)]
Expand All @@ -17,7 +17,10 @@ public class EquivalentToTest
[TestCase("A", "(empty)", false)]
[TestCase("(empty)", "", true)]
[TestCase("(empty)", "A", false)]
public void EquivalentTo_Text_Success(object value, string reference, bool expected)
[TestCase("(null)", null, true)]
[TestCase(null, null, true)]
[TestCase(null, "(null)", true)]
public void EquivalentTo_Text_Success(object? value, string? reference, bool expected)
{
var predicate = new EquivalentTo(() => reference);
Assert.Multiple(() =>
Expand All @@ -34,7 +37,10 @@ public void EquivalentTo_Text_Success(object value, string reference, bool expec
[TestCase("A", "(empty)", true)]
[TestCase("(empty)", "", false)]
[TestCase("(empty)", "A", false)]
public void SortedAfter_Text_Success(object value, string reference, bool expected)
[TestCase("(null)", null, false)]
[TestCase(null, null, false)]
[TestCase(null, "(null)", false)]
public void SortedAfter_Text_Success(object? value, string? reference, bool expected)
{
var predicate = new SortedAfter(() => reference);
Assert.Multiple(() =>
Expand All @@ -51,7 +57,10 @@ public void SortedAfter_Text_Success(object value, string reference, bool expect
[TestCase("A", "(empty)", true)]
[TestCase("(empty)", "", true)]
[TestCase("(empty)", "A", false)]
public void SortedAfterOrEquivalentTo_Text_Success(object value, string reference, bool expected)
[TestCase("(null)", null, true)]
[TestCase(null, null, true)]
[TestCase(null, "(null)", true)]
public void SortedAfterOrEquivalentTo_Text_Success(object? value, string? reference, bool expected)
{
var predicate = new SortedAfterOrEquivalentTo(() => reference);
Assert.Multiple(() =>
Expand All @@ -68,7 +77,10 @@ public void SortedAfterOrEquivalentTo_Text_Success(object value, string referenc
[TestCase("A", "(empty)", false)]
[TestCase("(empty)", "", false)]
[TestCase("(empty)", "A", true)]
public void SortedBefore_Text_Success(object value, string reference, bool expected)
[TestCase("(null)", null, false)]
[TestCase(null, null, false)]
[TestCase(null, "(null)", false)]
public void SortedBefore_Text_Success(object? value, string? reference, bool expected)
{
var predicate = new SortedBefore(() => reference);
Assert.Multiple(() =>
Expand All @@ -85,7 +97,10 @@ public void SortedBefore_Text_Success(object value, string reference, bool expec
[TestCase("A", "(empty)", false)]
[TestCase("(empty)", "", true)]
[TestCase("(empty)", "A", true)]
public void SortedBeforeOrEquivalentTo_Text_Success(object value, string reference, bool expected)
[TestCase("(null)", null, true)]
[TestCase(null, null, true)]
[TestCase(null, "(null)", true)]
public void SortedBeforeOrEquivalentTo_Text_Success(object? value, string? reference, bool expected)
{
var predicate = new SortedBeforeOrEquivalentTo(() => reference);
Assert.Multiple(() =>
Expand Down
32 changes: 17 additions & 15 deletions Expressif/Functions/Text/CountingFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Expressif.Values.Special;

namespace Expressif.Functions.Text;

public abstract class BaseTextCountingFunction : BaseTextFunction
{
protected override object EvaluateSpecial(string value) => -1;
protected override object EvaluateBlank() => -1;
protected override object EvaluateEmpty() => 0;
protected override object EvaluateNull() => 0;
protected override object? EvaluateSpecial(string value) => null;
protected override object? EvaluateBlank() => null;
protected override object? EvaluateEmpty() => 0;
protected override object? EvaluateNull() => 0;
}

/// <summary>
Expand All @@ -21,19 +22,15 @@ public abstract class BaseTextCountingFunction : BaseTextFunction
[Function(aliases: ["count-chars"])]
public class Length : BaseTextCountingFunction
{
protected override object EvaluateSpecial(string value) => -1;
protected override object EvaluateBlank() => -1;
protected override object EvaluateEmpty() => 0;
protected override object EvaluateNull() => 0;
protected override object EvaluateString(string value) => value.Length;
protected override object? EvaluateString(string value) => value.Length;
}

/// <summary>
/// Returns the count of distinct chars in the textual argument value. If the value is `null` or `empty` then it returns `0`. If the value is `blank` then it returns `-1`.
/// </summary>
public class CountDistinctChars : BaseTextCountingFunction
{
protected override object EvaluateString(string value)
protected override object? EvaluateString(string value)
{
var chars = new List<char>(value.Length);
for (int i = 0; i < value.Length; i++)
Expand All @@ -48,15 +45,17 @@ protected override object EvaluateString(string value)
/// </summary>
public class CountSubstring : BaseTextCountingFunction
{
public Func<string> Substring { get; }
public Func<string?> Substring { get; }

/// <param name="substring">The substring to count in the argument value.</param>
public CountSubstring(Func<string> substring)
public CountSubstring(Func<string?> substring)
=> Substring = substring;

protected override object EvaluateString(string value)
protected override object? EvaluateString(string value)
{
var substring = Substring.Invoke();
if (substring is null || new Null().Equals(substring) || new Empty().Equals(substring))
return 0;
var index = 0;
var count = 0;
do
Expand All @@ -71,6 +70,9 @@ protected override object EvaluateString(string value)
while (index != -1 && index <= value.Length - substring.Length);
return count;
}

protected override object? EvaluateBlank()
=> (Substring?.Invoke() ?? string.Empty).ToCharArray().Any(x => !char.IsWhiteSpace(x)) ? 0 : null;
}

/// <summary>
Expand All @@ -86,8 +88,8 @@ public TokenCount()
public TokenCount(Func<char> separator)
=> Separator = separator;

protected override object EvaluateBlank() => 0;
protected override object EvaluateString(string value) => CountToken(value);
protected override object? EvaluateBlank() => 0;
protected override object? EvaluateString(string value) => CountToken(value);

private int CountToken(string value)
{
Expand Down
14 changes: 7 additions & 7 deletions Expressif/Functions/Text/TextFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ public abstract class BaseTextFunction : IFunction
};
}

private object EvaluateUncasted(object value)
private object? EvaluateUncasted(object value)
{
var caster = new TextCaster();
var str = caster.Cast(value);
return EvaluateHighLevelString(str);
}

protected virtual object EvaluateHighLevelString(string value)
protected virtual object? EvaluateHighLevelString(string value)
{
if (new Empty().Equals(value))
return EvaluateEmpty();
Expand All @@ -52,11 +52,11 @@ protected virtual object EvaluateHighLevelString(string value)
return EvaluateString(value);
}

protected virtual object EvaluateNull() => new Null().Keyword;
protected virtual object EvaluateEmpty() => new Empty().Keyword;
protected virtual object EvaluateBlank() => new Whitespace().Keyword;
protected virtual object EvaluateSpecial(string value) => value;
protected abstract object EvaluateString(string value);
protected virtual object? EvaluateNull() => new Null().Keyword;
protected virtual object? EvaluateEmpty() => new Empty().Keyword;
protected virtual object? EvaluateBlank() => new Whitespace().Keyword;
protected virtual object? EvaluateSpecial(string value) => value;
protected abstract object? EvaluateString(string value);
}

/// <summary>
Expand Down
10 changes: 5 additions & 5 deletions Expressif/Predicates/Text/BaseTextPredicate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ protected override bool EvaluateBaseText(string value)

public abstract class BaseTextPredicateReference : BaseTextPredicate
{
public Func<string> Reference { get; }
public Func<string?> Reference { get; }

public BaseTextPredicateReference(Func<string> reference)
public BaseTextPredicateReference(Func<string?> reference)
=> Reference = reference;

protected override bool EvaluateBaseText(string value)
{
if (new Values.Special.Null().Equals(value) || new Values.Special.Null().Equals(Reference.Invoke()))
if (new Null().Equals(value) || new Null().Equals(Reference.Invoke()))
return EvaluateNull();
if ((new Values.Special.Whitespace().Equals(value) || new Values.Special.Whitespace().Equals(Reference.Invoke()))
if ((new Whitespace().Equals(value) || new Whitespace().Equals(Reference.Invoke()))
&& !(new Values.Special.Empty().Equals(value) || new Values.Special.Empty().Equals(Reference.Invoke())))
return EvaluateWhitespaces();

Expand All @@ -80,4 +80,4 @@ protected override bool EvaluateBaseText(string value)

protected abstract bool EvaluateText(string value, string reference);
protected virtual bool EvaluateWhitespaces() => false;
}
}
29 changes: 19 additions & 10 deletions Expressif/Predicates/Text/Sorting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ public class EquivalentTo : BaseTextPredicateReference
protected StringComparer Comparer { get; }

/// <param name="reference">A string to be compared to the argument value.</param>
public EquivalentTo(Func<string> reference)
public EquivalentTo(Func<string?> reference)
: this(reference, StringComparer.InvariantCultureIgnoreCase) { }

/// <param name="reference">A string to be compared to the argument value.</param>
/// <param name="comparer">A definition of the parameters of the comparison (case-sensitivity, culture-sensitivity)..</param>
public EquivalentTo(Func<string> reference, StringComparer comparer)
public EquivalentTo(Func<string?> reference, StringComparer comparer)
: base(reference) { Comparer = comparer; }

protected override bool EvaluateNull()
=> new Null().Equals(Reference.Invoke()) || base.EvaluateNull();

protected override bool EvaluateText(string value, string reference)
=> Comparer.Compare(value, reference) == 0;
}
Expand All @@ -36,14 +39,17 @@ protected override bool EvaluateText(string value, string reference)
public class SortedAfter : EquivalentTo
{
/// <param name="reference">A string to be compared to the argument value.</param>
public SortedAfter(Func<string> reference)
public SortedAfter(Func<string?> reference)
: this(reference, StringComparer.InvariantCultureIgnoreCase) { }

/// <param name="reference">A string to be compared to the argument value.</param>
/// <param name="comparer">A definition of the parameters of the comparison (case-sensitivity, culture-sensitivity).</param>
public SortedAfter(Func<string> reference, StringComparer comparer)
public SortedAfter(Func<string?> reference, StringComparer comparer)
: base(reference, comparer) { }

protected override bool EvaluateNull()
=> false;

protected override bool EvaluateText(string value, string reference)
=> Comparer.Compare(value, reference) >0;
}
Expand All @@ -53,12 +59,12 @@ protected override bool EvaluateText(string value, string reference)
public class SortedAfterOrEquivalentTo : EquivalentTo
{
/// <param name="reference">A string to be compared to the argument value.</param>
public SortedAfterOrEquivalentTo(Func<string> reference)
public SortedAfterOrEquivalentTo(Func<string?> reference)
: this(reference, StringComparer.InvariantCultureIgnoreCase) { }

/// <param name="reference">A string to be compared to the argument value.</param>
/// <param name="comparer">A definition of the parameters of the comparison (case-sensitivity, culture-sensitivity).</param>
public SortedAfterOrEquivalentTo(Func<string> reference, StringComparer comparer)
public SortedAfterOrEquivalentTo(Func<string?> reference, StringComparer comparer)
: base(reference, comparer) { }

protected override bool EvaluateText(string value, string reference)
Expand All @@ -71,14 +77,17 @@ protected override bool EvaluateText(string value, string reference)
public class SortedBefore : EquivalentTo
{
/// <param name="reference">A string to be compared to the argument value.</param>
public SortedBefore(Func<string> reference)
public SortedBefore(Func<string?> reference)
: this(reference, StringComparer.InvariantCultureIgnoreCase) { }

/// <param name="reference">A string to be compared to the argument value.</param>
/// <param name="comparer">A definition of the parameters of the comparison (case-sensitivity, culture-sensitivity).</param>
public SortedBefore(Func<string> reference, StringComparer comparer)
public SortedBefore(Func<string?> reference, StringComparer comparer)
: base(reference, comparer) { }

protected override bool EvaluateNull()
=> false;

protected override bool EvaluateText(string value, string reference)
=> Comparer.Compare(value, reference) < 0;
}
Expand All @@ -89,12 +98,12 @@ protected override bool EvaluateText(string value, string reference)
public class SortedBeforeOrEquivalentTo : EquivalentTo
{
/// <param name="reference">A string to be compared to the argument value.</param>
public SortedBeforeOrEquivalentTo(Func<string> reference)
public SortedBeforeOrEquivalentTo(Func<string?> reference)
: this(reference, StringComparer.InvariantCultureIgnoreCase) { }

/// <param name="reference">A string to be compared to the argument value.</param>
/// <param name="comparer">A definition of the parameters of the comparison (case-sensitivity, culture-sensitivity).</param>
public SortedBeforeOrEquivalentTo(Func<string> reference, StringComparer comparer)
public SortedBeforeOrEquivalentTo(Func<string?> reference, StringComparer comparer)
: base(reference, comparer) { }

protected override bool EvaluateText(string value, string reference)
Expand Down

0 comments on commit e504197

Please sign in to comment.