Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<Product>Simpra</Product>
<Company>ALTA Software llc.</Company>
<Copyright>Copyright © 2024 ALTA Software llc.</Copyright>
<Version>1.0.15</Version>
<Version>1.0.16</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ Simpra provides a set of internal functions out-of-the-box:
- **Strings**
- `number(str)` – converts to number
- `date(str)` – converts to date
- `substring(str, index, count)` – Retrieves part of a string starting at a **1-based position**.
---

### Extended Membership Operators
Expand Down
5 changes: 4 additions & 1 deletion src/AltaSoft.Simpra/InternalLanguageFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ internal static class InternalLanguageFunctions
// ReSharper disable InconsistentNaming
#pragma warning disable IDE1006 // Naming Styles

public static SimpraString substring(SimpraString input, SimpraNumber index) => SimpraString.Substring(input, index);
public static SimpraString substring(SimpraString input, SimpraNumber index, SimpraNumber count) => SimpraString.Substring(input, index, count);
public static SimpraNumber length(SimpraString input) => new(input.Value.Length);
public static SimpraNumber length(SimpraString[] input) => new(input.Length);
public static SimpraNumber length(SimpraNumber[] input) => new(input.Length);
Expand Down Expand Up @@ -48,7 +50,8 @@ internal static class InternalLanguageFunctions
nameof(sum),
nameof(@string),
nameof(@number),
nameof(@date)
nameof(@date),
nameof(substring)
];

internal static bool IsSimpraBuiltInFunction(this string name) => s_builtInFunctions.Contains(name);
Expand Down
21 changes: 16 additions & 5 deletions src/AltaSoft.Simpra/Types/SimpraString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,26 @@ public static SimpraString Substring(SimpraString value, SimpraNumber index, Sim
if (!value.HasValue)
return value;

var startIndex = (int)index.Value;
var startIndex = (int)index.Value - 1; // convert 1-based to 0-based index
var length = (int)count.Value;

return startIndex < 0 || length < 0 || startIndex + length > value.Value.Length
? throw new InvalidOperationException("The index and count must be within the bounds of the string")
: new SimpraString(value.Value.Substring(startIndex, length));
if (startIndex < 0)
startIndex = 0;

if (length < 0)
length = 0;

if (startIndex >= value.Value.Length || length == 0)
return new SimpraString(string.Empty);

if (startIndex + length >= value.Value.Length)
{
length = value.Value.Length - startIndex;
}
return new SimpraString(value.Value.Substring(startIndex, length));
}

public static SimpraString Substring(SimpraString value, SimpraNumber index) => !value.HasValue ? value : new SimpraString(value.Value.Substring((int)index.Value, 1));
public static SimpraString Substring(SimpraString value, SimpraNumber index) => Substring(value, index, 1);

public static SimpraBool Like(SimpraString value, SimpraString pattern)
{
Expand Down
165 changes: 165 additions & 0 deletions tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,171 @@ namespace AltaSoft.Simpra.Tests;

public class SimpraExpressionTests
{
[Theory]
// basic / within bounds
[InlineData("USD", 1, 3, "USD")] // from U
[InlineData("USD", 1, 2, "US")]
[InlineData("USD", 2, 2, "SD")] // from S
[InlineData("USD", 3, 1, "D")] // from D

// length clamping at the end
[InlineData("USD", 1, 10003, "USD")]
[InlineData("USD", 3, 10, "D")]

// start beyond end
[InlineData("USD", 1000, 1, "")]
[InlineData("USD", 4, 1, "")] // beyond "USD"

// start exactly at end
[InlineData("USD", 4, 0, "")]
[InlineData("USD", 4, 10, "")]

// zero length
[InlineData("USD", 1, 0, "")]
[InlineData("USD", 2, 0, "")]
[InlineData("USD", 3, 0, "")]

// negative/zero start → clamp to 1
[InlineData("USD", 0, 1, "U")]
[InlineData("USD", -5, 2, "US")]
[InlineData("USD", -2, 100, "USD")]

// negative length → empty
[InlineData("USD", 1, -1, "")]
[InlineData("USD", 3, -10, "")]
[InlineData("USD", -2, -10, "")]

// empty input
[InlineData("", 1, 5, "")]
[InlineData("", 10, 1, "")]
[InlineData("", -3, 2, "")]
[InlineData("", 1, 0, "")]

// whitespace
[InlineData(" ", 1, 1, " ")]
[InlineData(" ", 2, 2, " ")]
[InlineData(" ", 2, 100, " ")]

// non-ASCII (safe checks with multi-byte chars)
[InlineData("თბილისი", 1, 2, "თბ")]
[InlineData("თბილისი", 3, 3, "ილი")]
[InlineData("თბილისი", 11, 5, "")]
[InlineData("თბილისი", -3, 100, "თბილისი")]

public void BuiltInFunction_Substring_EdgeCases(string input, int start, int length, string expected)
{
var simpra = new Simpra();
var model = GetTestModel();
model.Ccy = input;
var expressionCode = $"return substring(Ccy,{start},{length})";
var result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), expressionCode);

Assert.Equal(expected, result);
}

[Fact]
public void BuiltInFunction_Substring_ShouldMatchExamplesFromPrompt()
{
var simpra = new Simpra();
var model = GetTestModel();
model.Ccy = "USD";

var result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), "return substring(Ccy,0,3)");
Assert.Equal("USD", result);

result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), "return substring(Ccy,0,10003)");
Assert.Equal("USD", result);

result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), "return substring(Ccy,1000,1)");
Assert.Equal("", result);

result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), "return substring(Ccy,1000,0)");
Assert.Equal("", result);

result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), "return substring(Ccy,1,0)");
Assert.Equal("", result);
}
[Theory]
// basic in-bounds
[InlineData("USD", 1, "U")]
[InlineData("USD", 2, "S")]
[InlineData("USD", 3, "D")]

// at/after end => empty
[InlineData("USD", 4, "")]
[InlineData("USD", 1000, "")]
[InlineData("U", 2, "")]
[InlineData("", 1, "")]
[InlineData("", 5, "")]

// zero/negative start → clamp to 1
[InlineData("USD", 0, "U")]
[InlineData("USD", -1, "U")]
[InlineData("USD", -5, "U")]
[InlineData("", -3, "")]

// whitespace
[InlineData(" X ", 1, " ")]
[InlineData(" X ", 2, "X")]
[InlineData(" X ", 3, " ")]

// non-ASCII (single UTF-16 code units)
[InlineData("თბილისი", 1, "თ")]
[InlineData("თბილისი", 3, "ი")]
[InlineData("თბილისი", 6, "ს")]
[InlineData("თბილისი", 7, "ი")]
[InlineData("თბილისი", 8, "")]

// very large index
[InlineData("USD", int.MaxValue, "")]
public void BuiltInFunction_Substring_StartOnly_EdgeCases(string input, int start, string expected)
{
var simpra = new Simpra();
var model = GetTestModel();
model.Ccy = input;
var expressionCode = $"return substring(Ccy,{start})";
var result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), expressionCode);

Assert.Equal(expected, result);
}

[Fact]
public void BuiltInFunction_Substring_StartOnly_NullSource_ShouldBeEmpty()
{
var simpra = new Simpra();
var model = GetTestModel();
model.Ccy = null;

var result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), "return substring(Ccy,1)");
Assert.Equal(null, result);
}

[Fact]
public void BuiltInFunction_Substring_NullSource_ShouldBeNull()
{
// If your DSL defines a behavior for nulls, keep this.
// If it should throw instead, change to Assert.Throws.
var simpra = new Simpra();
var model = GetTestModel();
model.Ccy = null!;

var result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), "return substring(Ccy,0,3)");
Assert.Null(result);
}

[Fact]
public void BuiltInFunction_Substring_ShouldReturnCorrectSubstringOfLength1()
{
const string expressionCode = "return substring(Ccy,1)";

var simpra = new Simpra();
var model = GetTestModel();
model.Ccy = "USD";

var result = simpra.Execute<string, TestModel, IFunctions>(model, new TestFunctions(), expressionCode);
Assert.Equal("U", result);
}

[Fact]
public void InvalidSimpraSyntax_ShouldThrowException_WhenIncorrectAndSignIsUsedAndReturnStatement()
{
Expand Down
Loading