Skip to content

Latest commit

 

History

History
683 lines (554 loc) · 18.3 KB

File metadata and controls

683 lines (554 loc) · 18.3 KB

StringUtils

Where are the project files?

Description

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/

Classes

StringUtils

This class acts as a namespace for free functions.

class NAMESPACE_su
{
  <<StringUtilsBody>>
}

join

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"));
}

repeat

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) == ""));
}

boolToString

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"));
}

isWordDelimiter

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)));
}

splitByWords

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"));
}

highlight

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)));

getCodePointAt

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));
}

Description

class NAMESPACE_Description
{
  <<DescriptionBody>>
  private Array<string> mFields;
}

Optimization opportunity: add… functions append to a private string, not an array. Compose simply returns it.

compose

string compose() { return NAMESPACE_su.join(mFields); }
{
  let d = new("Description");
  it("description: empty", Assert(d.compose() == ""));
}

add

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"));
}

addObject

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"));
}

addClass

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"));
}

addBool

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"));
}

addInt

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"));
}

addFloat

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"));
}

addDamageFlags

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"));
}

addCvar

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());
}

addSpac

/// 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"));
}

addLine

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"));
}

addSectorPart

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"));
}

addSector

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"));
}

addVector3

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"));
}

addState

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));
}

Ascii

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)));
}

Tests

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