From 9e2d6273311e93afffa823eae5bca73b61d20853 Mon Sep 17 00:00:00 2001 From: Temo Nikolaishvili Date: Tue, 23 Sep 2025 13:20:10 +0400 Subject: [PATCH] Add substring function Enhanced the `substring` function: - Added two overloads: with and without length parameter. - Improved handling of edge cases: - 1-based to 0-based index conversion. - Clamping of negative indices and lengths. - Handling of out-of-bounds indices and lengths. Updated `README.md` to document the `substring` function. --- Directory.Build.props | 2 +- README.md | 1 + .../InternalLanguageFunctions.cs | 5 +- src/AltaSoft.Simpra/Types/SimpraString.cs | 21 ++- .../SimpraExpressionTests.cs | 165 ++++++++++++++++++ 5 files changed, 187 insertions(+), 7 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 822cbde..6852b86 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,7 +9,7 @@ Simpra ALTA Software llc. Copyright © 2024 ALTA Software llc. - 1.0.15 + 1.0.16 diff --git a/README.md b/README.md index c72354f..aee7fc0 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/AltaSoft.Simpra/InternalLanguageFunctions.cs b/src/AltaSoft.Simpra/InternalLanguageFunctions.cs index d4f6f1d..bcdef88 100644 --- a/src/AltaSoft.Simpra/InternalLanguageFunctions.cs +++ b/src/AltaSoft.Simpra/InternalLanguageFunctions.cs @@ -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); @@ -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); diff --git a/src/AltaSoft.Simpra/Types/SimpraString.cs b/src/AltaSoft.Simpra/Types/SimpraString.cs index 1b9e517..d7ebf6c 100644 --- a/src/AltaSoft.Simpra/Types/SimpraString.cs +++ b/src/AltaSoft.Simpra/Types/SimpraString.cs @@ -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) { diff --git a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs index f270f16..3d99ba6 100644 --- a/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs +++ b/tests/AltaSoft.Simpra.tests/SimpraExpressionTests.cs @@ -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(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(model, new TestFunctions(), "return substring(Ccy,0,3)"); + Assert.Equal("USD", result); + + result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,0,10003)"); + Assert.Equal("USD", result); + + result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1000,1)"); + Assert.Equal("", result); + + result = simpra.Execute(model, new TestFunctions(), "return substring(Ccy,1000,0)"); + Assert.Equal("", result); + + result = simpra.Execute(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(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(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(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(model, new TestFunctions(), expressionCode); + Assert.Equal("U", result); + } + [Fact] public void InvalidSimpraSyntax_ShouldThrowException_WhenIncorrectAndSignIsUsedAndReturnStatement() {