parin/source/popka/core/ascii.d
2024-08-04 11:42:42 +03:00

878 lines
22 KiB
D

// Copyright 2024 Alexandros F. G. Kapretsos
// SPDX-License-Identifier: MIT
/// The `ascii` module provides functions designed to assist with ascii strings.
module popka.core.ascii;
import popka.core.containers;
import popka.core.errors;
import popka.core.traits;
import popka.core.types;
@safe @nogc nothrow:
enum digitChars = "0123456789";
enum upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
enum lowerChars = "abcdefghijklmnopqrstuvwxyz";
enum alphaChars = upperChars ~ lowerChars;
enum spaceChars = " \t\v\r\n\f";
version (Windows) {
enum pathSep = '\\';
enum otherPathSep = '/';
} else {
enum pathSep = '/';
enum otherPathSep = '\\';
}
struct ToStrOptions {
ubyte doublePrecision = 2;
}
bool isDigit(char c) {
return c >= '0' && c <= '9';
}
bool isDigit(IStr str) {
foreach (c; str) {
if (!isDigit(c)) return false;
}
return true;
}
bool isUpper(char c) {
return c >= 'A' && c <= 'Z';
}
bool isUpper(IStr str) {
foreach (c; str) {
if (!isUpper(c)) return false;
}
return true;
}
bool isLower(char c) {
return c >= 'a' && c <= 'z';
}
bool isLower(IStr str) {
foreach (c; str) {
if (!isLower(c)) return false;
}
return true;
}
bool isAlpha(char c) {
return isLower(c) || isUpper(c);
}
bool isAlpha(IStr str) {
foreach (c; str) {
if (!isAlpha(c)) return false;
}
return true;
}
bool isSpace(char c) {
foreach (sc; spaceChars) {
if (c == sc) return true;
}
return false;
}
bool isSpace(IStr str) {
foreach (c; str) {
if (!isSpace(c)) return false;
}
return true;
}
bool isCStr(IStr str) {
return str.length != 0 && str[$ - 1] == '\0';
}
char toUpper(char c) {
return isLower(c) ? cast(char) (c - 32) : c;
}
void toUpper(Str str) {
foreach (ref c; str) {
c = toUpper(c);
}
}
char toLower(char c) {
return isUpper(c) ? cast(char) (c + 32) : c;
}
void toLower(Str str) {
foreach (ref c; str) {
c = toLower(c);
}
}
@trusted
Sz length(ICStr str) {
Sz result = 0;
while (str[result] != '\0') {
result += 1;
}
return result;
}
bool equals(IStr str, IStr other) {
return str == other;
}
bool equals(IStr str, char other) {
return equals(str, charToStr(other));
}
bool equalsNoCase(IStr str, IStr other) {
if (str.length != other.length) return false;
foreach (i; 0 .. str.length) {
if (toUpper(str[i]) != toUpper(other[i])) return false;
}
return true;
}
bool equalsNoCase(IStr str, char other) {
return equalsNoCase(str, charToStr(other));
}
bool startsWith(IStr str, IStr start) {
if (str.length < start.length) return false;
return str[0 .. start.length] == start;
}
bool startsWith(IStr str, char start) {
return startsWith(str, charToStr(start));
}
bool endsWith(IStr str, IStr end) {
if (str.length < end.length) return false;
return str[$ - end.length .. $] == end;
}
bool endsWith(IStr str, char end) {
return endsWith(str, charToStr(end));
}
int count(IStr str, IStr item) {
int result = 0;
if (str.length < item.length || item.length == 0) return result;
foreach (i; 0 .. str.length - item.length) {
if (str[i .. i + item.length] == item) {
result += 1;
i += item.length - 1;
}
}
return result;
}
int count(IStr str, char item) {
return count(str, charToStr(item));
}
int findStart(IStr str, IStr item) {
if (str.length < item.length || item.length == 0) return -1;
foreach (i; 0 .. str.length - item.length + 1) {
if (str[i .. i + item.length] == item) return cast(int) i;
}
return -1;
}
int findStart(IStr str, char item) {
return findStart(str, charToStr(item));
}
int findEnd(IStr str, IStr item) {
if (str.length < item.length || item.length == 0) return -1;
foreach_reverse (i; 0 .. str.length - item.length + 1) {
if (str[i .. i + item.length] == item) return cast(int) i;
}
return -1;
}
int findEnd(IStr str, char item) {
return findEnd(str, charToStr(item));
}
IStr trimStart(IStr str) {
IStr result = str;
while (result.length > 0) {
if (isSpace(result[0])) result = result[1 .. $];
else break;
}
return result;
}
IStr trimEnd(IStr str) {
IStr result = str;
while (result.length > 0) {
if (isSpace(result[$ - 1])) result = result[0 .. $ - 1];
else break;
}
return result;
}
IStr trim(IStr str) {
return str.trimStart().trimEnd();
}
IStr removePrefix(IStr str, IStr prefix) {
if (str.startsWith(prefix)) {
return str[prefix.length .. $];
} else {
return str;
}
}
IStr removeSuffix(IStr str, IStr suffix) {
if (str.endsWith(suffix)) {
return str[0 .. $ - suffix.length];
} else {
return str;
}
}
IStr advance(IStr str, Sz amount) {
if (str.length < amount) {
return str[$ .. $];
} else {
return str[amount .. $];
}
}
void copyChars(Str str, IStr source, Sz startIndex = 0) {
if (str.length < source.length) {
assert(0, "The destination string `{}` must be at least as long as the source string `{}`.".format(str, source));
}
foreach (i, c; source) {
str[startIndex + i] = c;
}
}
void copy(ref Str str, IStr source, Sz startIndex = 0) {
copyChars(str, source, startIndex);
str = str[0 .. startIndex + source.length];
}
IStr pathDir(IStr path) {
auto end = findEnd(path, pathSep);
if (end == -1) {
return ".";
} else {
return path[0 .. end];
}
}
IStr pathConcat(IStr[] args...) {
static char[1024][4] buffers = void;
static byte bufferIndex = 0;
if (args.length == 0) {
return ".";
}
bufferIndex = (bufferIndex + 1) % buffers.length;
auto result = buffers[bufferIndex][];
auto length = 0;
foreach (i, arg; args) {
result.copyChars(arg, length);
length += arg.length;
if (i != args.length - 1) {
result.copyChars(charToStr(pathSep), length);
length += 1;
}
}
result = result[0 .. length];
return result;
}
IStr skipValue(ref inout(char)[] str, IStr separator) {
if (str.length < separator.length || separator.length == 0) {
str = str[$ .. $];
return "";
}
foreach (i; 0 .. str.length - separator.length) {
if (str[i .. i + separator.length] == separator) {
auto line = str[0 .. i];
str = str[i + separator.length .. $];
return line;
}
}
auto line = str[0 .. $];
if (str[$ - separator.length .. $] == separator) {
line = str[0 .. $ - 1];
}
str = str[$ .. $];
return line;
}
IStr skipValue(ref inout(char)[] str, char separator) {
return skipValue(str, charToStr(separator));
}
IStr skipLine(ref inout(char)[] str) {
return skipValue(str, '\n');
}
IStr boolToStr(bool value) {
return value ? "true" : "false";
}
IStr charToStr(char value) {
static char[1] buffer = void;
auto result = buffer[];
result[0] = value;
result = result[0 .. 1];
return result;
}
IStr unsignedToStr(ulong value) {
static char[64] buffer = void;
auto result = buffer[];
if (value == 0) {
result[0] = '0';
result = result[0 .. 1];
} else {
auto digitCount = 0;
for (auto temp = value; temp != 0; temp /= 10) {
result[$ - 1 - digitCount] = (temp % 10) + '0';
digitCount += 1;
}
result = result[$ - digitCount .. $];
}
return result;
}
IStr signedToStr(long value) {
static char[64] buffer = void;
auto result = buffer[];
if (value < 0) {
auto temp = unsignedToStr(-value);
result[0] = '-';
result.copy(temp, 1);
} else {
auto temp = unsignedToStr(value);
result.copy(temp, 0);
}
return result;
}
IStr doubleToStr(double value, ulong precision = 2) {
static char[64] buffer = void;
if (precision == 0) {
return signedToStr(cast(long) value);
}
auto result = buffer[];
auto cleanNumber = value;
auto rightDigitCount = 0;
while (cleanNumber != cast(double) (cast(long) cleanNumber)) {
rightDigitCount += 1;
cleanNumber *= 10;
}
// Add extra zeros at the end if needed.
// I do this because it makes it easier to remove the zeros later.
if (precision > rightDigitCount) {
foreach (j; 0 .. precision - rightDigitCount) {
rightDigitCount += 1;
cleanNumber *= 10;
}
}
// Digits go in the buffer from right to left.
auto cleanNumberStr = signedToStr(cast(long) cleanNumber);
auto i = result.length;
// Check two cases: 0.NN, N.NN
if (cast(long) value == 0) {
i -= cleanNumberStr.length;
result.copyChars(cleanNumberStr, i);
foreach (j; 0 .. rightDigitCount - cleanNumberStr.length) {
i -= 1;
result[i] = '0';
}
i -= 2;
result.copyChars("0.", i);
} else {
i -= rightDigitCount;
result.copyChars(cleanNumberStr[$ - rightDigitCount .. $], i);
i -= 1;
result[i] = '.';
i -= cleanNumberStr.length - rightDigitCount;
result.copyChars(cleanNumberStr[0 .. $ - rightDigitCount], i);
}
// Remove extra zeros at the end if needed.
if (precision < rightDigitCount) {
result = result[0 .. $ - rightDigitCount + precision];
}
return result[i .. $];
}
@trusted
IStr cStrToStr(ICStr value) {
return value[0 .. value.length];
}
IStr enumToStr(T)(T value) {
static char[64] buffer = void;
auto result = buffer[];
auto name = "";
final switch (value) {
static foreach (member; __traits(allMembers, T)) {
mixin("case T." ~ member ~ ": name = member; goto switchExit;");
}
}
switchExit:
foreach (i, c; name) {
result[i] = c;
}
result = result[0 .. name.length];
return result;
}
IStr toStr(T)(T value, ToStrOptions options = ToStrOptions()) {
static if (isCharType!T) {
return charToStr(value);
} else static if (isBoolType!T) {
return boolToStr(value);
} else static if (isUnsignedType!T) {
return unsignedToStr(value);
} else static if (isSignedType!T) {
return signedToStr(value);
} else static if (isDoubleType!T) {
return doubleToStr(value, options.doublePrecision);
} else static if (isStrType!T) {
return value;
} else static if (isCStrType!T) {
return cStrToStr(value);
} else static if (isEnumType!T) {
return enumToStr(value);
} else static if (__traits(hasMember, T, "toStr")) {
return value.toStr();
} else {
static assert(0, "The `toStr` function does not handle the `" ~ T.stringof ~ "` type. Implement a `toStr` function for that type.");
}
}
BasicResult!bool toBool(IStr str) {
auto result = BasicResult!bool();
if (str == "true") {
result.value = true;
} else if (str == "false") {
result.value = false;
} else {
result.error = BasicError.invalid;
}
return result;
}
ulong toBoolWithNone(IStr str) {
auto conv = toBool(str);
if (conv.error) {
return false;
} else {
return conv.value;
}
}
BasicResult!ulong toUnsigned(IStr str) {
auto result = BasicResult!ulong();
if (str.length == 0) {
result.error = BasicError.invalid;
} else if (str.length >= 18) {
result.error = BasicError.overflow;
} else {
ulong level = 1;
foreach_reverse (i, c; str) {
if (!isDigit(c)) {
result.error = BasicError.invalid;
break;
}
auto digit = c - '0';
result.value += digit * level;
level *= 10;
}
}
return result;
}
BasicResult!ulong toUnsigned(char c) {
auto result = BasicResult!ulong();
if (isDigit(c)) {
result.value = c - '0';
} else {
result.error = BasicError.invalid;
}
return result;
}
ulong toUnsignedWithNone(IStr str) {
auto conv = toUnsigned(str);
if (conv.error) {
return 0;
} else {
return conv.value;
}
}
ulong toUnsignedWithNone(char c) {
auto conv = toUnsigned(c);
if (conv.error) {
return 0;
} else {
return conv.value;
}
}
BasicResult!long toSigned(IStr str) {
auto result = BasicResult!long();
if (str.length == 0) {
result.error = BasicError.invalid;
} else if (str.length >= 18) {
result.error = BasicError.overflow;
} else {
if (str[0] == '-') {
auto conv = toUnsigned(str[1 .. $]);
if (conv.error) {
result.error = conv.error;
} else {
result.value = -conv.value;
}
} else {
auto conv = toUnsigned(str);
if (conv.error) {
result.error = conv.error;
} else {
result.value = conv.value;
}
}
}
return result;
}
BasicResult!long toSigned(char c) {
auto result = BasicResult!long();
auto conv = toUnsigned(c);
if (conv.error) {
result.error = conv.error;
} else {
result.value = cast(long) conv.value;
}
return result;
}
long toSignedWithNone(IStr str) {
auto conv = toSigned(str);
if (conv.error) {
return 0;
} else {
return conv.value;
}
}
long toSignedWithNone(char c) {
auto conv = toSigned(c);
if (conv.error) {
return 0;
} else {
return conv.value;
}
}
BasicResult!double toDouble(IStr str) {
auto result = BasicResult!double();
result.value = 0.0;
auto hasDot = false;
foreach (i, c; str) {
if (c == '.') {
hasDot = true;
auto lhs = toSigned(str[0 .. i]);
if (lhs.error) {
result.error = lhs.error;
} else {
auto rhs = toSigned(str[i + 1 .. $]);
if (rhs.error) {
result.error = rhs.error;
} else {
auto rhsLevel = 10;
foreach (_; 1 .. str[i + 1 .. $].length) {
rhsLevel *= 10;
}
result.value = lhs.value + ((lhs.value < 0 ? -1 : 1) * rhs.value / cast(double) rhsLevel);
}
}
break;
}
}
if (!hasDot) {
auto conv = toSigned(str);
result.value = conv.value;
result.error = conv.error;
}
return result;
}
double toDoubleWithNone(IStr str) {
auto conv = toDouble(str);
if (conv.error) {
return 0.0;
} else {
return conv.value;
}
}
BasicResult!T toEnum(T)(IStr str) {
auto result = BasicResult!T();
switch (str) {
static foreach (member; __traits(allMembers, T)) {
mixin("case " ~ member.stringof ~ ": result.value = T." ~ member ~ "; goto switchExit;");
}
default: result.error = BasicError.invalid;
}
switchExit:
return result;
}
T toEnumWithNone(T)(IStr str) {
auto conv = toEnum!T(str);
if (conv.error) {
return T.init;
} else {
return conv.value;
}
}
@trusted
ICStr toCStr(IStr str) {
static char[1024] buffer = void;
auto result = buffer[];
foreach (i, c; str) {
result[i] = c;
}
result[str.length] = '\0';
return result.ptr;
}
// TODO: Check if the args count is the same with the `{}` count.
IStr format(A...)(IStr formatStr, A args) {
static char[1024][8] buffers = void;
static byte bufferIndex = 0;
bufferIndex = (bufferIndex + 1) % buffers.length;
auto result = buffers[bufferIndex][];
auto resultIndex = 0;
auto formatStrIndex = 0;
auto argIndex = 0;
while (formatStrIndex < formatStr.length) {
auto c1 = formatStr[formatStrIndex];
auto c2 = formatStrIndex + 1 >= formatStr.length ? '+' : formatStr[formatStrIndex + 1];
if (c1 == '{' && c2 == '}' && argIndex < args.length) {
static foreach (i, arg; args) {
if (i == argIndex) {
auto temp = toStr(arg);
foreach (i, c; temp) {
result[resultIndex + i] = c;
}
resultIndex += temp.length;
formatStrIndex += 2;
argIndex += 1;
goto loopExit;
}
}
loopExit:
} else {
result[resultIndex] = c1;
resultIndex += 1;
formatStrIndex += 1;
}
}
result = result[0 .. resultIndex];
return result;
}
// TODO: Check if the args count is the same with the `{}` count.
void formatl(A...)(ref LStr text, IStr formatStr, A args) {
text.clear();
auto formatStrIndex = 0;
auto argIndex = 0;
while (formatStrIndex < formatStr.length) {
auto c1 = formatStr[formatStrIndex];
auto c2 = formatStrIndex + 1 >= formatStr.length ? '+' : formatStr[formatStrIndex + 1];
if (c1 == '{' && c2 == '}' && argIndex < args.length) {
static foreach (i, arg; args) {
if (i == argIndex) {
auto temp = toStr(arg);
foreach (i, c; temp) {
text.append(c);
}
formatStrIndex += 2;
argIndex += 1;
goto loopExit;
}
}
loopExit:
} else {
text.append(c1);
formatStrIndex += 1;
}
}
}
// Function test.
@trusted
unittest {
assert(isDigit("0123456789?") == false);
assert(isDigit("0123456789") == true);
assert(isUpper("hello") == false);
assert(isUpper("HELLO") == true);
assert(isLower("HELLO") == false);
assert(isLower("hello") == true);
assert(isSpace(" \t\r\n ") == true);
assert(isCStr("hello") == false);
assert(isCStr("hello\0") == true);
char[128] buffer = void;
Str str;
str = buffer[];
str.copy("Hello");
assert(str == "Hello");
str.toUpper();
assert(str == "HELLO");
str.toLower();
assert(str == "hello");
str = buffer[];
str.copy("Hello\0");
assert(isCStr(str) == true);
assert(str.ptr.length + 1 == str.length);
str = buffer[];
str.copy("Hello");
assert(str.equals("HELLO") == false);
assert(str.equalsNoCase("HELLO") == true);
assert(str.startsWith("H") == true);
assert(str.startsWith("Hell") == true);
assert(str.startsWith("Hello") == true);
assert(str.endsWith("o") == true);
assert(str.endsWith("ello") == true);
assert(str.endsWith("Hello") == true);
str = buffer[];
str.copy("hello hello world.");
assert(str.count("hello") == 2);
assert(str.findStart("HELLO") == -1);
assert(str.findStart("hello") == 0);
assert(str.findEnd("HELLO") == -1);
assert(str.findEnd("hello") == 6);
str = buffer[];
str.copy(" Hello world. ");
assert(str.trimStart() == "Hello world. ");
assert(str.trimEnd() == " Hello world.");
assert(str.trim() == "Hello world.");
assert(str.removePrefix("Hello") == str);
assert(str.trim().removePrefix("Hello") == " world.");
assert(str.removeSuffix("world.") == str);
assert(str.trim().removeSuffix("world.") == "Hello ");
assert(str.advance(0) == str);
assert(str.advance(1) == str[1 .. $]);
assert(str.advance(str.length) == "");
assert(str.advance(str.length + 1) == "");
assert(pathConcat("one", "two").pathDir() == "one");
assert(pathConcat("one").pathDir() == ".");
str = buffer[];
str.copy("one, two ,three,");
assert(skipValue(str, ',') == "one");
assert(skipValue(str, ',') == " two ");
assert(skipValue(str, ',') == "three");
assert(skipValue(str, ',') == "");
assert(str.length == 0);
assert(boolToStr(false) == "false");
assert(boolToStr(true) == "true");
assert(charToStr('L') == "L");
assert(unsignedToStr(0) == "0");
assert(unsignedToStr(69) == "69");
assert(signedToStr(0) == "0");
assert(signedToStr(69) == "69");
assert(signedToStr(-69) == "-69");
assert(signedToStr(-69) == "-69");
assert(doubleToStr(0.00, 0) == "0");
assert(doubleToStr(0.00, 1) == "0.0");
assert(doubleToStr(0.00, 2) == "0.00");
assert(doubleToStr(0.00, 3) == "0.000");
assert(doubleToStr(0.60, 1) == "0.6");
assert(doubleToStr(0.60, 2) == "0.60");
assert(doubleToStr(0.60, 3) == "0.600");
assert(doubleToStr(0.09, 1) == "0.0");
assert(doubleToStr(0.09, 2) == "0.09");
assert(doubleToStr(0.09, 3) == "0.090");
assert(doubleToStr(69.0, 1) == "69.0");
assert(doubleToStr(69.0, 2) == "69.00");
assert(doubleToStr(69.0, 3) == "69.000");
assert(cStrToStr("Hello\0") == "Hello");
enum TestEnum {
one,
two,
}
assert(enumToStr(TestEnum.one) == "one");
assert(enumToStr(TestEnum.two) == "two");
// TODO: ToValue tests write need!!!
auto text1 = "1.0";
auto conv1 = toDouble(text1);
assert(conv1.value == 1.0);
assert(conv1.error == BasicError.none);
auto text2 = "1";
auto conv2 = toDouble(text2);
assert(conv2.value == 1.0);
assert(conv2.error == BasicError.none);
auto text3 = "1?";
auto conv3 = toDouble(text3);
assert(conv3.value == 0.0);
assert(conv3.error == BasicError.invalid);
assert(format("") == "");
assert(format("{}") == "{}");
assert(format("{}", "1") == "1");
assert(format("{} {}", "1", "2") == "1 2");
assert(format("{} {} {}", "1", "2", "3") == "1 2 3");
assert(format("{} {} {}", 1, -2, 3.69) == "1 -2 3.69");
assert(format("{}", 420, 320, 220, 120, 20) == "420");
assert(format("", 1, -2, 3.69) == "");
assert(format("({})", format("({}, {})", false, true)) == "((false, true))");
// TODO: Uncoment when the N.00 bug is fixed.
// assert(format("{}", 0.00) == "0.00");
// assert(format("{}", 0.50) == "0.50");
// assert(format("{}", 1.00) == "1.00");
// assert(format("{}", 1.50) == "1.50");
}