From 88a11034b9735785e54f25ddd8c79a2a0786ce98 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Tue, 23 Jun 2026 20:00:02 +0100 Subject: [PATCH] stdlib: add SplitChar / SplitLines / JoinList to StrUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StrUtils had JoinStr (open-array join) but no splitter at all — a gap any text-processing code hits. Add SplitChar (split on a delimiter byte), SplitLines (split on LF, tolerating CRLF), and JoinList (the list-valued inverse of SplitChar), all returning/taking TList. This pulls Generics.Collections into StrUtils' interface. Lifted from the Luhmann web server. Adds 10 tests (57 stdlib tests total). --- stdlib/src/main/pascal/strutils.pas | 83 ++++++++++++++ stdlib/src/test/pascal/strutils.tests.pas | 134 ++++++++++++++++++++++ stdlib/src/test/pascal/test.registry.pas | 3 +- 3 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 stdlib/src/test/pascal/strutils.tests.pas diff --git a/stdlib/src/main/pascal/strutils.pas b/stdlib/src/main/pascal/strutils.pas index 73bb6ed..19474ee 100644 --- a/stdlib/src/main/pascal/strutils.pas +++ b/stdlib/src/main/pascal/strutils.pas @@ -24,6 +24,9 @@ unit StrUtils; interface +uses + Generics.Collections; { TList for the Split/Join helpers } + { ------------------------------------------------------------------ } { Containment } @@ -131,6 +134,18 @@ function IsEmptyOrWhitespace(const S: string): Boolean; { Joins Parts with Sep between each element. } function JoinStr(const Sep: string; const Parts: array of string): string; +{ Split S on a single delimiter byte, returning the pieces (one more piece + than there are delimiters; empty pieces are kept). Caller owns the list. } +function SplitChar(const S: string; ADelim: Byte): TList; + +{ Split S into lines on LF, tolerating CRLF (a trailing CR is stripped from + each line). Caller owns the list. } +function SplitLines(const S: string): TList; + +{ Join the items of AList with ASep between them (the list-valued inverse of + SplitChar; cf. JoinStr for an open array). nil yields ''. } +function JoinList(AList: TList; const ASep: string): string; + { ------------------------------------------------------------------ } { UTF-8 Codepoint operations } { ------------------------------------------------------------------ } @@ -666,6 +681,74 @@ begin SB.Free(); end; +function SplitChar(const S: string; ADelim: Byte): TList; +var + I, N, Start: Integer; +begin + Result := TList.Create(); + N := Length(S); + Start := 0; + I := 0; + while I < N do + begin + if Byte(S[I]) = ADelim then + begin + Result.Add(Copy(S, Start, I - Start)); + Start := I + 1; + end; + I := I + 1; + end; + Result.Add(Copy(S, Start, N - Start)); +end; + +{ Copy S[AStart..AEnd) as a line, dropping one trailing CR so a CRLF line + yields the bare text. } +function LineSlice(const S: string; AStart, AEnd: Integer): string; +begin + if (AEnd > AStart) and (Byte(S[AEnd - 1]) = 13) then { trailing CR } + Result := Copy(S, AStart, AEnd - AStart - 1) + else + Result := Copy(S, AStart, AEnd - AStart); +end; + +function SplitLines(const S: string): TList; +var + I, N, Start: Integer; +begin + Result := TList.Create(); + N := Length(S); + Start := 0; + I := 0; + while I < N do + begin + if Byte(S[I]) = 10 then { LF } + begin + Result.Add(LineSlice(S, Start, I)); + Start := I + 1; + end; + I := I + 1; + end; + Result.Add(LineSlice(S, Start, N)); +end; + +function JoinList(AList: TList; const ASep: string): string; +var + SB: TStringBuilder; + I: Integer; +begin + if AList = nil then + Exit(''); + SB := TStringBuilder.Create(); + for I := 0 to AList.Count - 1 do + begin + if I > 0 then + SB.Append(ASep); + SB.Append(AList.Get(I)); + end; + Result := SB.ToString(); + SB.Free(); +end; + { ------------------------------------------------------------------ } { TStringBuilder } { ------------------------------------------------------------------ } diff --git a/stdlib/src/test/pascal/strutils.tests.pas b/stdlib/src/test/pascal/strutils.tests.pas new file mode 100644 index 0000000..4159a8e --- /dev/null +++ b/stdlib/src/test/pascal/strutils.tests.pas @@ -0,0 +1,134 @@ +{ + Blaise - An Object Pascal Compiler + Copyright (c) 2026 Graeme Geldenhuys + SPDX-License-Identifier: Apache-2.0 WITH Swift-exception + Licensed under the Apache License v2.0 with Runtime Library Exception. + See LICENSE file in the project root for full license terms. +} + +{ Tests for the StrUtils split/join helpers (SplitChar / SplitLines / + JoinList). Self-registers via the initialization section. } + +unit StrUtils.Tests; + +interface + +uses + blaise.testing, StrUtils, Generics.Collections; + +type + TStrUtilsTests = class(TTestCase) + published + procedure TestSplitChar_Basic; + procedure TestSplitChar_EmptyPieces; + procedure TestSplitChar_NoDelim; + procedure TestSplitChar_Empty; + procedure TestSplitLines_LF; + procedure TestSplitLines_CRLF; + procedure TestSplitLines_TrailingNewline; + procedure TestJoinList_Basic; + procedure TestJoinList_Empty; + procedure TestRoundTrip; + end; + +implementation + +const + CH_COMMA = 44; + +procedure TStrUtilsTests.TestSplitChar_Basic; +var L: TList; +begin + L := SplitChar('a,b,c', CH_COMMA); + AssertEquals('count', 3, Integer(L.Count)); + AssertEquals('0', 'a', L.Get(0)); + AssertEquals('1', 'b', L.Get(1)); + AssertEquals('2', 'c', L.Get(2)); +end; + +procedure TStrUtilsTests.TestSplitChar_EmptyPieces; +var L: TList; +begin + { leading, trailing and adjacent delimiters keep empty pieces } + L := SplitChar(',a,,b,', CH_COMMA); + AssertEquals('count', 5, Integer(L.Count)); + AssertEquals('0', '', L.Get(0)); + AssertEquals('1', 'a', L.Get(1)); + AssertEquals('2', '', L.Get(2)); + AssertEquals('3', 'b', L.Get(3)); + AssertEquals('4', '', L.Get(4)); +end; + +procedure TStrUtilsTests.TestSplitChar_NoDelim; +var L: TList; +begin + L := SplitChar('abc', CH_COMMA); + AssertEquals('count', 1, Integer(L.Count)); + AssertEquals('0', 'abc', L.Get(0)); +end; + +procedure TStrUtilsTests.TestSplitChar_Empty; +var L: TList; +begin + L := SplitChar('', CH_COMMA); + AssertEquals('count', 1, Integer(L.Count)); + AssertEquals('0', '', L.Get(0)); +end; + +procedure TStrUtilsTests.TestSplitLines_LF; +var L: TList; +begin + L := SplitLines('one'#10'two'#10'three'); + AssertEquals('count', 3, Integer(L.Count)); + AssertEquals('0', 'one', L.Get(0)); + AssertEquals('2', 'three', L.Get(2)); +end; + +procedure TStrUtilsTests.TestSplitLines_CRLF; +var L: TList; +begin + { CRLF lines: the trailing CR must be stripped } + L := SplitLines('one'#13#10'two'#13#10'three'); + AssertEquals('count', 3, Integer(L.Count)); + AssertEquals('0', 'one', L.Get(0)); + AssertEquals('1', 'two', L.Get(1)); + AssertEquals('2', 'three', L.Get(2)); +end; + +procedure TStrUtilsTests.TestSplitLines_TrailingNewline; +var L: TList; +begin + { a trailing LF yields a final empty line } + L := SplitLines('a'#10'b'#10); + AssertEquals('count', 3, Integer(L.Count)); + AssertEquals('last', '', L.Get(2)); +end; + +procedure TStrUtilsTests.TestJoinList_Basic; +var L: TList; +begin + L := TList.Create(); + L.Add('a'); L.Add('b'); L.Add('c'); + AssertEquals('joined', 'a-b-c', JoinList(L, '-')); +end; + +procedure TStrUtilsTests.TestJoinList_Empty; +var L: TList; +begin + L := TList.Create(); + AssertEquals('empty list', '', JoinList(L, '-')); + AssertEquals('nil list', '', JoinList(nil, '-')); +end; + +procedure TStrUtilsTests.TestRoundTrip; +var L: TList; +begin + { JoinList is the inverse of SplitChar on the same delimiter } + L := SplitChar('x,y,z', CH_COMMA); + AssertEquals('roundtrip', 'x,y,z', JoinList(L, ',')); +end; + +initialization + RegisterTest(TStrUtilsTests); + +end. diff --git a/stdlib/src/test/pascal/test.registry.pas b/stdlib/src/test/pascal/test.registry.pas index d38b6ae..b40bc38 100644 --- a/stdlib/src/test/pascal/test.registry.pas +++ b/stdlib/src/test/pascal/test.registry.pas @@ -27,7 +27,8 @@ uses Sockets.Tests, WebSockets.Tests, HttpServer.Tests, - Guid.Tests; + Guid.Tests, + StrUtils.Tests; implementation