StringUtils

Table of Contents

Where are the project files?

1. 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/

2. Classes

2.1. StringUtils

This class acts as a namespace for free functions.

class NAMESPACE_su
{

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

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

static clearscope string boolToString(bool value)
{
  return value ? "true" : "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;
}

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

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

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

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

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

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

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

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

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

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

2.2. Description

class NAMESPACE_Description
{

  string compose() { return NAMESPACE_su.join(mFields); }

  NAMESPACE_Description add(string name, string value)
  {
    mFields.push(name .. ": " .. value);
    return self;
  }

  NAMESPACE_Description addObject(string name, Object anObject)
  {
    if (anObject == NULL) return add(name, "NULL");

    string className = anObject.getClassName();
    return add(name, className);
  }

  NAMESPACE_Description addClass(string name, Class aClass)
  {
    if (aClass == NULL) return add(name, "NULL");
    return add(name, aClass.getClassName());
  }

  NAMESPACE_Description addBool(string name, bool value)
  {
    return add(name, NAMESPACE_su.boolToString(value));
  }

  NAMESPACE_Description addInt(string name, int value)
  {
    return add(name, string.format("%d", value));
  }

  NAMESPACE_Description addFloat(string name, double value)
  {
    return add(name, string.format("%.2f", value));
  }

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

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

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

  NAMESPACE_Description addLine(string name, Line aLine)
  {
    return addInt(name, aLine.index());
  }

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

  NAMESPACE_Description addSector(string name, Sector aSector)
  {
    return addInt(name, aSector.index());
  }

  NAMESPACE_Description addVector3(string name, vector3 vector)
  {
    return add(name, string.format("%.2f, %.2f, %.2f", vector.x, vector.y, vector.z));
  }

  NAMESPACE_Description addState(string name, State aState)
  {
    return add(name, new("NAMESPACE_Description").
               addInt("sprite", aState.sprite).
               addInt("frame", aState.Frame).compose());
  }
  private Array<string> mFields;
}

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

2.2.1. compose

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2.3. Ascii

class NAMESPACE_Ascii
{
  enum _
  {
    CHARACTER_NULL = 0,
    START_OF_HEADING = 1, // 0x1
    START_OF_TEXT = 2, // 0x2
    END_OF_TEXT = 3, // 0x3
    END_OF_TRANSMISSION = 4, // 0x4
    ENQUIRY = 5, // 0x5
    ACKNOWLEDGE = 6, // 0x6
    BELL = 7, // 0x7
    BACKSPACE = 8, // 0x8
    CHARACTER_TABULATION = 9, // 0x9
    LINE_FEED_LF = 10, // 0xA
    LINE_TABULATION = 11, // 0xB
    FORM_FEED_FF = 12, // 0xC
    CARRIAGE_RETURN_CR = 13, // 0xD
    SHIFT_OUT = 14, // 0xE
    SHIFT_IN = 15, // 0xF
    DATA_LINK_ESCAPE = 16, // 0x10
    DEVICE_CONTROL_ONE = 17, // 0x11
    DEVICE_CONTROL_TWO = 18, // 0x12
    DEVICE_CONTROL_THREE = 19, // 0x13
    DEVICE_CONTROL_FOUR = 20, // 0x14
    NEGATIVE_ACKNOWLEDGE = 21, // 0x15
    SYNCHRONOUS_IDLE = 22, // 0x16
    END_OF_TRANSMISSION_BLOCK = 23, // 0x17
    CANCEL = 24, // 0x18
    END_OF_MEDIUM = 25, // 0x19
    SUBSTITUTE = 26, // 0x1A
    ESCAPE = 27, // 0x1B
    INFORMATION_SEPARATOR_FOUR = 28, // 0x1C
    INFORMATION_SEPARATOR_THREE = 29, // 0x1D
    INFORMATION_SEPARATOR_TWO = 30, // 0x1E
    INFORMATION_SEPARATOR_ONE = 31, // 0x1F
    SPACE = 32, // 0x20
    EXCLAMATION_MARK = 33, // 0x21
    QUOTATION_MARK = 34, // 0x22
    NUMBER_SIGN = 35, // 0x23
    DOLLAR_SIGN = 36, // 0x24
    PERCENT_SIGN = 37, // 0x25
    AMPERSAND = 38, // 0x26
    APOSTROPHE = 39, // 0x27
    LEFT_PARENTHESIS = 40, // 0x28
    RIGHT_PARENTHESIS = 41, // 0x29
    ASTERISK = 42, // 0x2A
    PLUS_SIGN = 43, // 0x2B
    COMMA = 44, // 0x2C
    HYPHEN_MINUS = 45, // 0x2D
    FULL_STOP = 46, // 0x2E
    SOLIDUS = 47, // 0x2F
    DIGIT_ZERO = 48, // 0x30
    DIGIT_ONE = 49, // 0x31
    DIGIT_TWO = 50, // 0x32
    DIGIT_THREE = 51, // 0x33
    DIGIT_FOUR = 52, // 0x34
    DIGIT_FIVE = 53, // 0x35
    DIGIT_SIX = 54, // 0x36
    DIGIT_SEVEN = 55, // 0x37
    DIGIT_EIGHT = 56, // 0x38
    DIGIT_NINE = 57, // 0x39
    COLON = 58, // 0x3A
    SEMICOLON = 59, // 0x3B
    LESS_THAN_SIGN = 60, // 0x3C
    EQUALS_SIGN = 61, // 0x3D
    GREATER_THAN_SIGN = 62, // 0x3E
    QUESTION_MARK = 63, // 0x3F
    COMMERCIAL_AT = 64, // 0x40
    LATIN_CAPITAL_LETTER_A = 65, // 0x41
    LATIN_CAPITAL_LETTER_B = 66, // 0x42
    LATIN_CAPITAL_LETTER_C = 67, // 0x43
    LATIN_CAPITAL_LETTER_D = 68, // 0x44
    LATIN_CAPITAL_LETTER_E = 69, // 0x45
    LATIN_CAPITAL_LETTER_F = 70, // 0x46
    LATIN_CAPITAL_LETTER_G = 71, // 0x47
    LATIN_CAPITAL_LETTER_H = 72, // 0x48
    LATIN_CAPITAL_LETTER_I = 73, // 0x49
    LATIN_CAPITAL_LETTER_J = 74, // 0x4A
    LATIN_CAPITAL_LETTER_K = 75, // 0x4B
    LATIN_CAPITAL_LETTER_L = 76, // 0x4C
    LATIN_CAPITAL_LETTER_M = 77, // 0x4D
    LATIN_CAPITAL_LETTER_N = 78, // 0x4E
    LATIN_CAPITAL_LETTER_O = 79, // 0x4F
    LATIN_CAPITAL_LETTER_P = 80, // 0x50
    LATIN_CAPITAL_LETTER_Q = 81, // 0x51
    LATIN_CAPITAL_LETTER_R = 82, // 0x52
    LATIN_CAPITAL_LETTER_S = 83, // 0x53
    LATIN_CAPITAL_LETTER_T = 84, // 0x54
    LATIN_CAPITAL_LETTER_U = 85, // 0x55
    LATIN_CAPITAL_LETTER_V = 86, // 0x56
    LATIN_CAPITAL_LETTER_W = 87, // 0x57
    LATIN_CAPITAL_LETTER_X = 88, // 0x58
    LATIN_CAPITAL_LETTER_Y = 89, // 0x59
    LATIN_CAPITAL_LETTER_Z = 90, // 0x5A
    LEFT_SQUARE_BRACKET = 91, // 0x5B
    REVERSE_SOLIDUS = 92, // 0x5C
    RIGHT_SQUARE_BRACKET = 93, // 0x5D
    CIRCUMFLEX_ACCENT = 94, // 0x5E
    LOW_LINE = 95, // 0x5F
    GRAVE_ACCENT = 96, // 0x60
    LATIN_SMALL_LETTER_A = 97, // 0x61
    LATIN_SMALL_LETTER_B = 98, // 0x62
    LATIN_SMALL_LETTER_C = 99, // 0x63
    LATIN_SMALL_LETTER_D = 100, // 0x64
    LATIN_SMALL_LETTER_E = 101, // 0x65
    LATIN_SMALL_LETTER_F = 102, // 0x66
    LATIN_SMALL_LETTER_G = 103, // 0x67
    LATIN_SMALL_LETTER_H = 104, // 0x68
    LATIN_SMALL_LETTER_I = 105, // 0x69
    LATIN_SMALL_LETTER_J = 106, // 0x6A
    LATIN_SMALL_LETTER_K = 107, // 0x6B
    LATIN_SMALL_LETTER_L = 108, // 0x6C
    LATIN_SMALL_LETTER_M = 109, // 0x6D
    LATIN_SMALL_LETTER_N = 110, // 0x6E
    LATIN_SMALL_LETTER_O = 111, // 0x6F
    LATIN_SMALL_LETTER_P = 112, // 0x70
    LATIN_SMALL_LETTER_Q = 113, // 0x71
    LATIN_SMALL_LETTER_R = 114, // 0x72
    LATIN_SMALL_LETTER_S = 115, // 0x73
    LATIN_SMALL_LETTER_T = 116, // 0x74
    LATIN_SMALL_LETTER_U = 117, // 0x75
    LATIN_SMALL_LETTER_V = 118, // 0x76
    LATIN_SMALL_LETTER_W = 119, // 0x77
    LATIN_SMALL_LETTER_X = 120, // 0x78
    LATIN_SMALL_LETTER_Y = 121, // 0x79
    LATIN_SMALL_LETTER_Z = 122, // 0x7A
    LEFT_CURLY_BRACKET = 123, // 0x7B
    VERTICAL_LINE = 124, // 0x7C
    RIGHT_CURLY_BRACKET = 125, // 0x7D
    TILDE = 126, // 0x7E
    DELETE = 127, // 0x7F,
    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, // 0x%X" (string-replace "(" ""
                             (string-replace ")" ""
                             (string-replace "-" "_"
                             (string-replace " " "_"
                                             (char-to-name number))))) 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)));
}

Created: 2026-06-16 Tue 18:47