License: BSD-3-Clause
// SPDX-FileCopyrightText: © 2024 Alexander Kromm <mmaulwurff@gmail.com>
// SPDX-License-Identifier: BSD-3-Clause
// StringUtils module version: 1.4.0
// StringUtils is a part of DoomToolbox: https://github.com/mmaulwurff/doom-toolbox/This class acts as a namespace for free functions.
class NAMESPACE_su
{
<<StringUtilsBody>>
}Joins strings to a single string separated by delimiter.
static clearscope string join(Array<string> strings, string delimiter = ", ")
{
uint nStrings = strings.size();
if (nStrings == 0) return "";
string result = strings[0];
for (uint i = 1; i < nStrings; ++i)
result.appendFormat("%s%s", delimiter, strings[i]);
return result;
}{
Array<string> strings;
it("join: empty", Assert(su.join(strings, ".") == ""));
strings.push("hello");
it("join: one", Assert(su.join(strings) == "hello"));
strings.push("world");
it("join: default delimiter", Assert(su.join(strings) == "hello, world"));
it("join: empty delimiter", Assert(su.join(strings, "") == "helloworld"));
}Repeats the specified string the specified number of times.
static clearscope string repeat(string aString, int times)
{
// Make the specified number of spaces using padding format.
string result = string.format("%*d", times + 1, 0);
result.deleteLastCharacter();
result.replace(" ", aString);
return result;
}{
it("repeat: zero", Assert(su.repeat("a", 0) == ""));
it("repeat: one", Assert(su.repeat("hello", 1) == "hello"));
it("repeat: 3", Assert(su.repeat("!?", 3) == "!?!?!?"));
it("repeat: empty", Assert(su.repeat("", 7) == ""));
}Writes out a boolean value.
static clearscope string boolToString(bool value)
{
return value ? "true" : "false";
}{
it("boolToString: true", Assert(su.boolToString(true) == "true"));
it("boolToString: false", Assert(su.boolToString(false) == "false"));
}static clearscope bool isWordDelimiter(int character)
{
if (character == NAMESPACE_Ascii.HYPHEN_MINUS) return false;
if (character < NAMESPACE_Ascii.DIGIT_ZERO) return true;
if (NAMESPACE_Ascii.COLON <= character
&& character <= NAMESPACE_Ascii.COMMERCIAL_AT) return true;
if (NAMESPACE_Ascii.LEFT_SQUARE_BRACKET <= character
&& character <= NAMESPACE_Ascii.GRAVE_ACCENT) return true;
if (NAMESPACE_Ascii.LEFT_CURLY_BRACKET <= character
&& character <= NAMESPACE_Ascii.DELETE) return true;
// Various unicode spaces.
if (0x2000 <= character && character <= 0x200B) return true;
static const int unicodeDelimiters[] =
{
0x0085, // next line
0x00A0, // non-breaking space
0x2028, // line separator
0x2029, // paragraph separator
0x202F, // narrow non-breaking space
0x205F, // medium mathematical space
0x3000 // ideographic space
};
foreach (unicodeDelimiter : unicodeDelimiters)
if (character == unicodeDelimiter) return true;
return false;
}{
it("isWordDelimiter: space", Assert(su.isWordDelimiter(Ascii.SPACE)));
it("isWordDelimiter: comma", Assert(su.isWordDelimiter(Ascii.COMMA)));
it("isWordDelimiter: non-breaking space", Assert(su.isWordDelimiter(0x00A0)));
it("isWordDelimiter: a", Assert(!su.isWordDelimiter(Ascii.LATIN_SMALL_LETTER_A)));
it("isWordDelimiter: 0", Assert(!su.isWordDelimiter(Ascii.DIGIT_ZERO)));
it("isWordDelimiter: hyphen", Assert(!su.isWordDelimiter(Ascii.HYPHEN_MINUS)));
}Splits a UTF-8 text by words. Characters considered word delimiters: ASCII control characters, space, comma (‘,’), period (‘.’). Delimiters are not included in the output.
static clearscope void splitByWords(string text, out Array<string> words)
{
int length = text.length();
string currentWord;
for (int i = 0; i < length;)
{
int character;
[character, i] = text.getNextCodePoint(i);
if (!isWordDelimiter(character))
{
currentWord.appendCharacter(character);
continue;
}
if (currentWord.length() != 0)
{
words.push(currentWord);
currentWord = "";
}
}
if (currentWord.length() != 0)
words.push(currentWord);
}{
Array<string> words;
su.splitByWords("", words);
it("split empty", Assert(words.size() == 0));
} {
Array<string> words;
su.splitByWords("hello", words);
it("one word", Assert(words.size() == 1 && words[0] == "hello"));
} {
Array<string> words;
su.splitByWords("hello-world", words);
it("one word with -", Assert(words.size() == 1 && words[0] == "hello-world"));
} {
Array<string> words;
su.splitByWords(" hello world \n ", words);
it("two words, many spaces",
Assert(words.size() == 2 && words[0] == "hello" && words[1] == "world"));
}Highlights a character inside a string with a color.
static clearscope string highlight(string base, int index, int color)
{
if (index < 0) return base;
int baseLength = base.length();
int letterCode;
int position = 0;
for (int i = 0; i < index && position < baseLength; ++i)
[letterCode, position] = base.getNextCodePoint(position);
if (position == baseLength) return base;
string left = base.left(position);
[letterCode, position] = base.getNextCodePoint(position);
string right = base.mid(position, base.length() - position);
int colorCode = NAMESPACE_Ascii.LATIN_SMALL_LETTER_A + color;
return string.format("%s\c%c%c\c-%s", left, colorCode, letterCode, right);
}it("highlight -1", Assert("мышь" == su.highlight("мышь", -1, Font.CR_Tan)));
it("highlight 0", Assert("\cbм\c-ышь" == su.highlight("мышь", 0, Font.CR_Tan)));
it("highlight 1", Assert("м\cgы\c-шь" == su.highlight("мышь", 1, Font.CR_Red)));
it("highlight end", Assert("мыш\cgь\c-" == su.highlight("мышь", 3, Font.CR_Red)));
it("highlight 4" , Assert("мышь" == su.highlight("мышь", 4, Font.CR_Tan)));Finds a Unicode character by index. Attention: O(n).
static clearscope int getCodePointAt(string aString, int index)
{
int letterCode = 0;
int position = 0;
int stringLength = aString.length();
for (int i = 0; i <= index && position <= stringLength; ++i)
[letterCode, position] = aString.getNextCodePoint(position);
return letterCode;
}{
int codePoint0 = 0x43C; // 'м'
int codePoint1 = 0x44B; // 'ы'
int codePoint3 = 0x44C; // 'ь'
it("getCodePointAt 0", AssertEval(su.getCodePointAt("мышь", 0), "==", codePoint0));
it("getCodePointAt 1", AssertEval(su.getCodePointAt("мышь", 1), "==", codePoint1));
it("getCodePointAt 3", AssertEval(su.getCodePointAt("мышь", 3), "==", codePoint3));
it("getCodePointAt -1", AssertEval(su.getCodePointAt("мышь", -1), "==", 0));
it("getCodePointAt end", AssertEval(su.getCodePointAt("мышь", 4), "==", 0));
}class NAMESPACE_Description
{
<<DescriptionBody>>
private Array<string> mFields;
}Optimization opportunity: add… functions append to a private string, not an array. Compose simply returns it.
string compose() { return NAMESPACE_su.join(mFields); }{
let d = new("Description");
it("description: empty", Assert(d.compose() == ""));
}NAMESPACE_Description add(string name, string value)
{
mFields.push(name .. ": " .. value);
return self;
}{
let d = new("Description");
d.add("k1", "v1").add("k2", "v2");
it("description: two", Assert(d.compose() == "k1: v1, k2: v2"));
}NAMESPACE_Description addObject(string name, Object anObject)
{
if (anObject == NULL) return add(name, "NULL");
string className = anObject.getClassName();
return add(name, className);
}{
let d = new("Description");
Object o;
d.addObject("n", o).addObject("self", self);
it("description: object", Assert(d.compose() == "n: NULL, self: su_Test"));
}NAMESPACE_Description addClass(string name, Class aClass)
{
if (aClass == NULL) return add(name, "NULL");
return add(name, aClass.getClassName());
}{
string result = new("Description").addClass("c", self.getClass()).compose();
it("description: class", Assert(result == "c: su_Test"));
}NAMESPACE_Description addBool(string name, bool value)
{
return add(name, NAMESPACE_su.boolToString(value));
}{
let d = new("Description");
d.addBool("b", true);
it("description: bool", Assert(d.compose() == "b: true"));
}NAMESPACE_Description addInt(string name, int value)
{
return add(name, string.format("%d", value));
}{
let d = new("Description");
d.addInt("value", -19);
it("description: int", Assert(d.compose() == "value: -19"));
}NAMESPACE_Description addFloat(string name, double value)
{
return add(name, string.format("%.2f", value));
}{
let d = new("Description");
d.addFloat("value", -19.4);
it("description: float", Assert(d.compose() == "value: -19.40"));
}NAMESPACE_Description addDamageFlags(string name, EDmgFlags flags)
{
Array<string> results;
if (flags & DMG_NO_ARMOR) results.push("DMG_NO_ARMOR");
if (flags & DMG_INFLICTOR_IS_PUFF) results.push("DMG_INFLICTOR_IS_PUFF");
if (flags & DMG_THRUSTLESS) results.push("DMG_THRUSTLESS");
if (flags & DMG_FORCED) results.push("DMG_FORCED");
if (flags & DMG_NO_FACTOR) results.push("DMG_NO_FACTOR");
if (flags & DMG_PLAYERATTACK) results.push("DMG_PLAYERATTACK");
if (flags & DMG_FOILINVUL) results.push("DMG_FOILINVUL");
if (flags & DMG_FOILBUDDHA) results.push("DMG_FOILBUDDHA");
if (flags & DMG_NO_PROTECT) results.push("DMG_NO_PROTECT");
if (flags & DMG_USEANGLE) results.push("DMG_USEANGLE");
if (flags & DMG_NO_PAIN) results.push("DMG_NO_PAIN");
if (flags & DMG_EXPLOSION) results.push("DMG_EXPLOSION");
if (flags & DMG_NO_ENHANCE) results.push("DMG_NO_ENHANCE");
return add(name, NAMESPACE_su.join(results));
}{
let d = new("Description");
d.addDamageFlags("d", DMG_NO_ARMOR | DMG_NO_ENHANCE);
it("description: damage", Assert(d.compose() == "d: DMG_NO_ARMOR, DMG_NO_ENHANCE"));
}NAMESPACE_Description addCvar(string name)
{
let aCvar = Cvar.getCvar(name, players[consolePlayer]);
if (aCvar == NULL) return add(name, "NULL");
switch (aCvar.getRealType())
{
case Cvar.CVAR_Bool: return addBool(name, NAMESPACE_su.boolToString(aCvar.getInt()));
case Cvar.CVAR_Int: return addInt(name, aCvar.getInt());
case Cvar.CVAR_Float: return addFloat(name, aCvar.getFloat());
case Cvar.CVAR_String: return add(name, aCvar.getString());
// TODO: implement color:
case Cvar.CVAR_Color: return addInt(name, aCvar.getInt());
}
return add(name, string.format("unknown type (%d)", aCvar.getRealType()));
}{
let d = new("Description");
Array<string> cvars = { "su_bool", "su_int", "su_float", "su_string" };
foreach (aCvar : cvars) d.addCvar(aCvar);
bool isPassing = d.compose()
== "su_bool: true, su_int: 3, su_float: -7.5, su_string: \"♥test\"";
it("description: cvar", Assert(isPassing));
if (!isPassing) Console.printf("%s", d.compose());
}/// SPAC - special activation types.
NAMESPACE_Description addSpac(string name, int flags)
{
Array<string> results;
if (flags & SPAC_Cross) results.push("SPAC_Cross");
if (flags & SPAC_Use) results.push("SPAC_Use");
if (flags & SPAC_MCross) results.push("SPAC_MCross");
if (flags & SPAC_Impact) results.push("SPAC_Impact");
if (flags & SPAC_Push) results.push("SPAC_Push");
if (flags & SPAC_PCross) results.push("SPAC_PCross");
if (flags & SPAC_UseThrough) results.push("SPAC_UseThrough");
if (flags & SPAC_AnyCross) results.push("SPAC_AnyCross");
if (flags & SPAC_MUse) results.push("SPAC_MUse");
if (flags & SPAC_MPush) results.push("SPAC_MPush");
if (flags & SPAC_UseBack) results.push("SPAC_UseBack");
if (flags & SPAC_Damage) results.push("SPAC_Damage");
if (flags & SPAC_Death) results.push("SPAC_Death");
return add(name, NAMESPACE_su.join(results));
}{
let d = new("Description");
d.addSpac("s", SPAC_Cross | SPAC_Death);
it("description: SPAC", Assert(d.compose() == "s: SPAC_Cross, SPAC_Death"));
}NAMESPACE_Description addLine(string name, Line aLine)
{
return addInt(name, aLine.index());
}{
let d = new("Description");
d.addLine("l", level.lines[1]);
it("description: line", Assert(d.compose() == "l: 1"));
}NAMESPACE_Description addSectorPart(string name, SectorPart part)
{
switch (part)
{
case SECPART_None: return add(name, "SECPART_None");
case SECPART_Floor: return add(name, "SECPART_Floor");
case SECPART_Ceiling: return add(name, "SECPART_Ceiling");
case SECPART_3D: return add(name, "SECPART_3D");
}
return add(name, string.format("unknown SECPART (%d)", part));
}{
let d = new("Description");
d.addSectorPart("s", SECPART_3D);
it("description: SECPART", Assert(d.compose() == "s: SECPART_3D"));
}NAMESPACE_Description addSector(string name, Sector aSector)
{
return addInt(name, aSector.index());
}{
let d = new("Description");
d.addSector("s", level.sectors[1]);
it("description: sector", Assert(d.compose() == "s: 1"));
}NAMESPACE_Description addVector3(string name, vector3 vector)
{
return add(name, string.format("%.2f, %.2f, %.2f", vector.x, vector.y, vector.z));
}{
let d = new("Description");
vector3 v = (1.1, 2.2, 3.3);
d.addVector3("v", v);
it("description: vector", Assert(d.compose() == "v: 1.10, 2.20, 3.30"));
}NAMESPACE_Description addState(string name, State aState)
{
return add(name, new("NAMESPACE_Description").
addInt("sprite", aState.sprite).
addInt("frame", aState.Frame).compose());
}{
let d = new("Description");
let state = players[consolePlayer].ReadyWeapon.FindState("Fire");
d.addState("s", state);
string expected = string.format("s: sprite: %d, frame: %d",
state.sprite,
state.Frame);
it("description: state", Assert(d.compose() == expected));
}class NAMESPACE_Ascii
{
enum _
{
CHARACTER_NULL = 0,
<<generate-table()>>,
END
}
const FIRST_PRINTABLE = SPACE;
const CASE_DIFFERENCE = LATIN_SMALL_LETTER_A - LATIN_CAPITAL_LETTER_A;
static bool isControlCharacter(int code)
{
return code < NAMESPACE_Ascii.SPACE || code == NAMESPACE_Ascii.DELETE;
}
}(defun number-to-ascii-enum (number)
(format "%s = %d" (string-replace "(" ""
(string-replace ")" ""
(string-replace "-" "_"
(string-replace " " "_" (char-to-name number))))) number))
(mapconcat 'number-to-ascii-enum (number-sequence 1 127) ",\n"){
it("ASCII tab", Assert("\t" == string.format("%c", Ascii.CHARACTER_TABULATION)));
it("ASCII \\n", Assert("\n" == string.format("%c", Ascii.LINE_FEED_LF)));
it("ASCII ',' is not a control character",
Assert(!Ascii.isControlCharacter(Ascii.COMMA)));
it("ASCII '\\n' is a control character",
Assert(Ascii.isControlCharacter(Ascii.LINE_FEED_LF)));
}version 4.14.3
#include "StringUtils.zs"version 4.14.3
class su : NAMESPACE_su {}
class Description : NAMESPACE_Description {}
class Ascii : NAMESPACE_Ascii {}
class su_Test : Clematis
{
override void TestSuites()
{
Describe("StringUtils tests");
<<TestsBody>>
EndDescribe();
}
}wait 2; map map01; wait 2; netevent test:su_Test; wait 2; quit