Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ public class DateOperationTests
[TestCase("NotBetween(Date(\"2023-01-05\"), Date(\"2023-01-10\"), Date(\"2023-01-01\"))", ExpectedResult = false)]
[TestCase("NotBetween(Date(\"2023-01-01\"), Date(\"2023-01-01\"), Date(\"2023-01-01\"))", ExpectedResult = false)]

// IsBetween with DateTime strings
[TestCase("IsBetween('2025-09-22', '2025-09-20', '2025-09-25')", ExpectedResult = true)]
[TestCase("IsBetween('2025-09-20', '2025-09-20', '2025-09-25')", ExpectedResult = true)]
[TestCase("IsBetween('2025-09-25', '2025-09-20', '2025-09-25')", ExpectedResult = true)]
[TestCase("IsBetween('2025-09-19', '2025-09-20', '2025-09-25')", ExpectedResult = false)]
[TestCase("IsBetween('2025-09-22', '2025-09-25', '2025-09-20')", ExpectedResult = false)]
[TestCase("IsBetween('2025-09-19', '2025-09-25', '2025-09-20')", ExpectedResult = false)]

// Subtract
[TestCase("SubtractDate(CreateDate(2020, 1, 4), CreateDate(2020, 1, 1))", ExpectedResult = 3)]
[TestCase("SubtractTime(CreateDate(2020, 1, 1, 11, 15, 0), CreateDate(2020, 1, 1, 10, 14, 0))", ExpectedResult = 61)]
Expand Down
69 changes: 61 additions & 8 deletions src/Albatross.Expression.Test/Operations/TextOperationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ public class TextOperationTests
[TestCase("padright(1, 3, 0)", ExpectedResult = "100")]
[TestCase("padright(1111, 3, 0)", ExpectedResult = "1111")]


// Right & Left
[TestCase("left(\"abc\", 2)", ExpectedResult = "ab")]
[TestCase("left(\"abc\", 0)", ExpectedResult = "")]
Expand Down Expand Up @@ -78,18 +77,36 @@ public class TextOperationTests
[TestCase("concat(true,5, 8,\"-variable\")", ExpectedResult = "58-variable")]

// Contains
[TestCase("contains(\"[Option one, Option two]\", \"[Option one]\")", ExpectedResult = true)]
[TestCase("contains(\"[Option one, Option two]\", \"[Option one, Option two]\")", ExpectedResult = true)]
[TestCase("contains(\"[Option one, Option two]\", \"[Option one, Option three]\")", ExpectedResult = false)]
[TestCase("contains(\"[Option one, Option two]\", \"Option three\")", ExpectedResult = false)]
[TestCase("contains(\"Option one\", \"Option one\")", ExpectedResult = true)]
[TestCase("contains(\"Option one\", \"Option two\")", ExpectedResult = false)]
[TestCase("Contains(\"Option one and two\", \"Option one\")", ExpectedResult = true)]
[TestCase("Contains(\"Option one and two\", \"two\")", ExpectedResult = true)]
[TestCase("Contains(\"Option one and two\", \"Option three\")", ExpectedResult = false)]
[TestCase("Contains(\"Option one\", \"option ONE\")", ExpectedResult = true)] // Case-insensitive
[TestCase("Contains(\"Hello world\", \"WORLD\")", ExpectedResult = true)]
[TestCase("Contains(\"Hello\", \"Hi\")", ExpectedResult = false)]
[TestCase("Contains(\"some text\", \"\")", ExpectedResult = false)] // empty substring

// Word Count markdown text
[TestCase("wordCount(\"**123**\")", ExpectedResult = 1)]
[TestCase("wordCount(\"_123_\")", ExpectedResult = 1)]
[TestCase("wordCount(\"**__123 123__**\")", ExpectedResult = 2)]
[TestCase("wordCount(\"### H3\\n\\nH2\\n--\\n\\nH1\\n==\")", ExpectedResult = 3)]
[TestCase("wordCount(\"**Bold **_Italic_ ~~StrikeThrough~~ **_~~BoldItalicStrikeThrough~~_****_ BoldItalic_** _~~ItalicStrikeThrough~~__ _**~~BoldStrikeThrough~~**\")", ExpectedResult = 7)]
[TestCase("wordCount(\"> Block Quote\\n\\n`Code`\\n\\n Code Block\\n\\nEmoji \\n\\n[Link](https://stackedit.io/app#)\")", ExpectedResult = 7)]
[TestCase("wordCount(\"1. LevelOne\\n\\n2. LevelTwo\\n\\n\\n\\n* LevelOne\\n\\n* LevelTwo\")", ExpectedResult = 4)]
[TestCase("wordCount(\"1. **Bold**\\n\\n2. **_BoldItalic_**\\n\\n3. **_~~BoldItalicStrikeThrough~~_**\\n\\n4. [Link](http://asdasdasdad@dasfv/asdqw)\\n\\n5. Emoji 2 \ud83d\ude07 \ud83d\ude17\")", ExpectedResult = 6)]

// Not Contains
[TestCase("NotContains(\"Option One\", \"Option two\")", ExpectedResult = true)]
[TestCase("NotContains(\"Option One\", \"Option One\")", ExpectedResult = false)]
[TestCase("NotContains(\"[Option One, Option two]\", \"Option One\")", ExpectedResult = false)]
[TestCase("NotContains(\"[Option One, Option two]\", \"Option three\")", ExpectedResult = true)]
[TestCase("NotContains(\"[Option One, Option two]\", \"Option three\")", ExpectedResult = true)]
[TestCase("wordCount(\"1. **Bold**\\n\\n2. **_BoldItalic_**\\n\\n3. **_~~BoldItalicStrikeThrough~~_**\\n\\n4. [Link](http://asdasdasdad@dasfv/asdqw)\\n\\n5. Emoji 2 \ud83d\ude07 \ud83d\ude17\")", ExpectedResult = 6)]
// NotContains
[TestCase("NotContains(\"Option One\", \"Option two\")", ExpectedResult = true)]
[TestCase("NotContains(\"Option One\", \"Option One\")", ExpectedResult = false)]
[TestCase("NotContains(\"Option One and Two\", \"Three\")", ExpectedResult = true)]
[TestCase("NotContains(\"The quick brown fox\", \"lazy dog\")", ExpectedResult = true)]
[TestCase("NotContains(\"The quick brown fox\", \"QUICK\")", ExpectedResult = false)] // case-insensitive

// In
[TestCase("In(\"Option one\", \"[Option one, Option two]\")", ExpectedResult = true)]
Expand All @@ -106,6 +123,42 @@ public class TextOperationTests
[TestCase("NotIn(\"[Option one, Option two]\", \"[Option one, Option two, Option three]\")", ExpectedResult = false)]
[TestCase("NotIn(\"Option two\", \"Option two\")", ExpectedResult = false)]
[TestCase("NotIn(\"Option three\", \"Option two\")", ExpectedResult = true)]

// IsBlank tests
[TestCase("IsBlank(\"\")", ExpectedResult = true)] // Empty string
[TestCase("IsBlank(\" \")", ExpectedResult = true)] // Whitespace string
[TestCase("IsBlank(\"Some text\")", ExpectedResult = false)] // Non-empty string
[TestCase("IsBlank(\" text \")", ExpectedResult = false)] // Whitespace around text
[TestCase("IsBlank(123)", ExpectedResult = false)] // Non-string, non-null input
[TestCase("IsBlank(\"true\")", ExpectedResult = false)] // Boolean input

// IsNotBlank tests
[TestCase("IsNotBlank(\"\")", ExpectedResult = false)] // Empty string
[TestCase("IsNotBlank(\" \")", ExpectedResult = false)] // Whitespace string
[TestCase("IsNotBlank(\"Some text\")", ExpectedResult = true)] // Non-empty string
[TestCase("IsNotBlank(\" text \")", ExpectedResult = true)] // Whitespace around text
[TestCase("IsNotBlank(123)", ExpectedResult = true)] // Non-string, non-null input
[TestCase("IsNotBlank(\"false\")", ExpectedResult = true)] // Boolean input

// Char Count
[TestCase("charCount(\"123\")", ExpectedResult = 3)]
[TestCase("charCount(\"123 123\")", ExpectedResult = 6)]
[TestCase("charCount(\"Word\")", ExpectedResult = 4)]
[TestCase("charCount(\"C Sharp\")", ExpectedResult = 6)]

// Char Count markdown text
[TestCase("charCount(\"**123**\")", ExpectedResult = 3)]
[TestCase("charCount(\"_123_\")", ExpectedResult = 3)]
[TestCase("charCount(\"**__123 123__**\")", ExpectedResult = 6)]
[TestCase("charCount(\"### H3\\n\\nH2\\n--\\n\\nH1\\n==\")", ExpectedResult = 6)]
[TestCase("charCount(\"**Bold **_Italic_ ~~StrikeThrough~~ **_~~BoldItalicStrikeThrough~~_****_ BoldItalic_** _~~ItalicStrikeThrough~~__ _**~~BoldStrikeThrough~~**\")", ExpectedResult = 96)]
[TestCase("charCount(\"> Block Quote\\n\\n`Code`\\n\\n Code Block\\n\\nEmoji \\n\\n[Link](https://stackedit.io/app#)\")", ExpectedResult = 32)]
[TestCase("charCount(\"1. LevelOne\\n\\n2. LevelTwo\\n\\n\\n\\n* LevelOne\\n\\n* LevelTwo\")", ExpectedResult = 32)]
[TestCase("charCount(\"1. **Bold**\\n\\n2. **_BoldItalic_**\\n\\n3. **_~~BoldItalicStrikeThrough~~_**\\n\\n4. [Link](http://asdasdasdad@dasfv/asdqw)\\n\\n5. Emoji 2 \ud83d\ude07 \ud83d\ude17\")", ExpectedResult = 47)]
[TestCase("charCount(\"[IRFAN](https://testqa.workiom.club/app/apps/b226f5a6-7a1a-41be-ac89-546bd3084bf5/list/1f815325-d905-49e5-8976-c26dafff4f89/394417) IRFAN\")", ExpectedResult = 10)]
[TestCase("charCount(\"IRFAN 💫\")", ExpectedResult = 5)]
[TestCase("charCount(\"IRFAN\\n=====\")", ExpectedResult = 5)]
[TestCase("charCount(\"> IRFAN\")", ExpectedResult = 5)]
public object OperationsTesting(string expression)
{
return Factory.Instance.Create().Compile(expression).EvalValue(null);
Expand Down
8 changes: 4 additions & 4 deletions src/Albatross.Expression/Albatross.Expression.csproj
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AssemblyVersion>2.3.35.0</AssemblyVersion>
<FileVersion>2.3.35.0</FileVersion>
<AssemblyVersion>2.3.48.0</AssemblyVersion>
<FileVersion>2.3.48.0</FileVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>2.3.35.0</Version>
<Version>2.3.48.0</Version>
<Authors>Workiom.Expression</Authors>
<PackageId>Workiom.Expression</PackageId>
<Product>Workiom.Expression</Product>
<Description>Add char and word count functions #2</Description>
<Description>Fix CreateQrCode Func Doc</Description>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Security.Principal.Windows">
Expand Down
123 changes: 82 additions & 41 deletions src/Albatross.Expression/Extension.cs

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions src/Albatross.Expression/Functions/File/CreateQRCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ namespace Albatross.Expression.Functions.File
{
[FunctionDoc(Group.File, "{token}( , , )",
@"### Returns a QR code in image format.
#### Inputs:
- input: String (mandatory). Accepts record fields or plain text.
- ecc: Error correction level (optional). Default is 'M'. Supported: 'L', 'M', 'Q', 'H'.
- size: Width and height of the image (optional, in pixels). Default is 256.
#### Inputs:
- input: String (mandatory). Accepts record fields or plain text.
- ecc: Error correction level (optional). Default is 'M'. Supported: 'L', 'M', 'Q', 'H'.
- size: Width and height of the image (optional, in pixels). Default is 256.

#### Outputs:
- File: The generated QR code image."
- File: The generated QR code image."
)]
[ParserOperation]
public class CreateQRCode : PrefixOperationToken
Expand Down
55 changes: 41 additions & 14 deletions src/Albatross.Expression/Functions/Number/IsBetween.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,27 @@
namespace Albatross.Expression.Functions.Number
{
[FunctionDoc(Group.Number, "{token}( , , )",
@"### Check if a number is between two values
@"### Check if a number or date is between two values

#### Inputs:
- @val1: Minimum value of the range
- @val2: Maximum value of the range
- @number: Number to check if it is in the range
#### Outputs:
- True if the number is between the two values (inclusive), otherwise false."
- @value: The number or date to check.
- @min: The minimum bound (number or date).
- @max: The maximum bound (number or date).

#### Behavior:
- Compares `value` against the `min` and `max` bounds.
- Returns `true` if `value` is between the bounds (inclusive).
- Automatically swaps bounds if `min > max`.
- Supports **either all numbers** or **all date values**.
- Throws an error if types are mixed or unrecognized.

#### Examples:
- `IsBetween(5, 1, 10)` → `true`
- `IsBetween(5, 10, 1)` → `true` (bounds swapped)
- `IsBetween('2025-09-22', '2025-09-20', '2025-09-25')` → `true`
- `IsBetween('2025-09-26', '2025-09-25', '2025-09-20')` → `false` (bounds swapped)"
)]

[ParserOperation]
public class IsBetween : PrefixOperationToken
{
Expand All @@ -25,16 +38,30 @@ public class IsBetween : PrefixOperationToken

public override object EvalValue(Func<string, object> context)
{
var input = Operands.Select(item => (int)Convert.ChangeType(item.EvalValue(context), typeof(int))).ToArray();
if (input.Length != 3)
{
var operands = Operands.Select(o => o.EvalValue(context)).ToArray();

if (operands.Length != 3)
throw new ArgumentException("IsBetween function requires exactly three operands.");

if (operands.All(o => DateTime.TryParse(o.ToString(), out _)))
{
var dates = operands.Select(o => DateTime.Parse(o.ToString())).ToArray();
var value = dates[0].Date;
var min = dates[1].Date < dates[2].Date ? dates[1].Date : dates[2].Date;
var max = dates[1].Date > dates[2].Date ? dates[1].Date : dates[2].Date;
return value >= min && value <= max;
}

var numberToCheck = input[0];
var minValue = input[1];
var maxValue = input[2];
if (operands.All(o => o is double))
{
var numbers = operands.Cast<double>().ToArray();
var value = numbers[0];
var min = Math.Min(numbers[1], numbers[2]);
var max = Math.Max(numbers[1], numbers[2]);
return value >= min && value <= max;
}

return numberToCheck >= minValue && numberToCheck <= maxValue; }
throw new ArgumentException("Operands must be either all doubles or all parsable dates.");
}
}
}
}
68 changes: 68 additions & 0 deletions src/Albatross.Expression/Functions/Text/CharCount.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Albatross.Expression.Documentation.Attributes;
using Albatross.Expression.Exceptions;
using Albatross.Expression.Tokens;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Group = Albatross.Expression.Documentation.Group;

namespace Albatross.Expression.Functions.Text
{
[FunctionDoc(Group.Text, "{token}( )",
@"### Returns the number of characters in a string.
#### Inputs:
- string: String
#### Outputs:
- Integer"
)]
[ParserOperation]
public class CharCount : PrefixOperationToken
{
public override string Name => "CharCount";
public override int MinOperandCount => 1;
public override int MaxOperandCount => 1;
public override bool Symbolic => false;

public override object EvalValue(Func<string, object> context)
{
List<object> list = GetOperands(context);

object value = list[0];
switch (value)
{
case null:
return null;
case string s:
return CountCharacters(s);
case ICollection collection:
return Convert.ToDouble(collection.Count);
default:
throw new UnexpectedTypeException(value.GetType());
}
}

private static double CountCharacters(string text)
{
if (string.IsNullOrEmpty(text))
return 0;

text = text.Trim();

var result = text.TryNormalizeText(out string normalizedText);
if (result)
text = normalizedText;

// Count ONLY alphanumeric characters (letters A-Z, a-z and numbers 0-9)
// Per requirements: Do NOT count apostrophes, spaces, punctuation, or special symbols
int count = 0;
foreach (var c in text)
{
if (char.IsLetter(c) || char.IsDigit(c))
count++;
}

return count;
}
}
}
52 changes: 19 additions & 33 deletions src/Albatross.Expression/Functions/Text/Contains.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
using Albatross.Expression.Documentation.Attributes;
using Albatross.Expression.Exceptions;
using Albatross.Expression.Tokens;
using System;
using System.Collections;
using System.Linq;
using System.Text;

namespace Albatross.Expression.Operations
namespace Albatross.Expression.Functions.Text
{
/// <summary>
/// <para>Prefix operation that will take an input and C# format string and produced a formatted string</para>
///
/// </summary>
[FunctionDoc(Documentation.Group.Text, "{token}( , )",
"### Check if operand2 is contained within operand1\n"+
"#### Inputs:\n"+
"- operand1: a array of values or a single value\n"+
"- operand2: a array of values or a value to be checked\n"+
"#### Outputs:\n"+
"- true if operand2 is included in operand1, false otherwise."
[FunctionDoc(Documentation.Group.Text, "{token}(text, substring)",
"### Checks if the `substring` is present in the given `text`\n" +
"#### Inputs:\n" +
"- **text**: The full string in which to search\n" +
"- **substring**: The string to look for within `text`\n" +
"#### Output:\n" +
"- `true` if `substring` is found inside `text` (case-insensitive), `false` otherwise."
)]
[ParserOperation]
public class Contains : PrefixOperationToken
Expand All @@ -33,27 +29,17 @@ public class Contains : PrefixOperationToken
public override object EvalValue(Func<string, object> context)
{
var operands = GetOperands(context);
var operand1 = operands[0].ToString().Trim('[', ']');
var operand2 = operands[1].ToString().Trim('[', ']');

if (operand1.Contains(",") || operand2.Contains(","))
{
var operand1List = operand1.Split(',').Select(s => s.Trim()).ToArray();
var operand2List = operand2.Split(',').Select(s => s.Trim()).ToArray();

foreach (var item in operand2List)
{
if (!operand1List.Contains(item))
return false;
}
}
else
{
if (!operand1.Contains(operand2))
return false;
}

return true;

if (operands.Count != 2)
throw new InvalidOperationException("Contains requires exactly 2 operands.");

var operand1 = operands[0]?.ToString();
var operand2 = operands[1]?.ToString();

if (string.IsNullOrEmpty(operand1) || string.IsNullOrEmpty(operand2))
return false;

return operand1.ToLower().Contains(operand2.ToLower());
}
}
}
Loading