using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using Kalk.Core.Helpers; using Scriban; using Scriban.Functions; using Scriban.Helpers; using Scriban.Runtime; using Scriban.Syntax; namespace Kalk.Core { /// /// Misc module (builtin). /// public sealed partial class MiscModule : KalkModuleWithFunctions { public const string CategoryMisc = "Misc Functions"; public static readonly Encoding EncodingExtendedAscii = CodePagesEncodingProvider.Instance.GetEncoding(1252); public MiscModule() { IsBuiltin = true; DateObject = new DateTimeFunctions {Format = "%D %T"}; RegisterFunctionsAuto(); } /// /// The date object used by the function. /// public DateTimeFunctions DateObject { get; } /// /// Gets the current date, parse the input date or return the date object, depending on use cases. /// - If this function doesn't have any parameter and is not used to index a member, it returns the current date. It is equivalent of `date.now` /// /// The return date object has the following properties: /// /// | Name | Description /// |-------------- |----------------- /// | `.year` | Gets the year of a date object /// | `.month` | Gets the month of a date object /// | `.day` | Gets the day in the month of a date object /// | `.day_of_year` | Gets the day within the year /// | `.hour` | Gets the hour of the date object /// | `.minute` | Gets the minute of the date object /// | `.second` | Gets the second of the date object /// | `.millisecond` | Gets the millisecond of the date object /// /// - If this function has a string parameter, it will try to parse the string as a date /// - Otherwise, if this function provides the following sub functions and members: /// - `date.add_days`: Example `'2016/01/05' |> date |> date.add_days 1` /// - `date.add_months`: Example `'2016/01/05' |> date |> date.add_months 1` /// - `date.add_years`: Example `'2016/01/05' |> date |> date.add_years 1` /// - `date.add_hours` /// - `date.add_minutes` /// - `date.add_seconds` /// - `date.add_milliseconds` /// - `date.to_string`: Converts a datetime object to a textual representation using the specified format string. /// /// By default, if you are using a date, it will use the format specified by `date.format` which defaults to `date.default_format` (readonly) which default to `%d %b %Y` /// /// You can override the format used for formatting all dates by assigning the a new format: `date.format = '%a %b %e %T %Y';` /// /// You can recover the default format by using `date.format = date.default_format;` /// /// By default, the to_string format is using the **current culture**, but you can switch to an invariant culture by using the modifier `%g` /// /// For example, using `%g %d %b %Y` will output the date using an invariant culture. /// /// If you are using `%g` alone, it will output the date with `date.format` using an invariant culture. /// /// Suppose that `date.now` would return the date `2013-09-12 22:49:27 +0530`, the following table explains the format modifiers: /// /// | Format | Result | Description /// |--------|-------------------|-------------------------------------------- /// | `"%a"` | `"Thu"` | Name of week day in short form of the /// | `"%A"` | `"Thursday"` | Week day in full form of the time /// | `"%b"` | `"Sep"` | Month in short form of the time /// | `"%B"` | `"September"` | Month in full form of the time /// | `"%c"` | | Date and time (%a %b %e %T %Y) /// | `"%C"` | `"20"` | Century of the time /// | `"%d"` | `"12"` | Day of the month of the time /// | `"%D"` | `"09/12/13"` | Date (%m/%d/%y) /// | `"%e"` | `"12"` | Day of the month, blank-padded ( 1..31) /// | `"%F"` | `"2013-09-12"` | ISO 8601 date (%Y-%m-%d) /// | `"%h"` | `"Sep"` | Alias for %b /// | `"%H"` | `"22"` | Hour of the time in 24 hour clock format /// | `"%I"` | `"10"` | Hour of the time in 12 hour clock format /// | `"%j"` | `"255"` | Day of the year (001..366) (3 digits, left padded with zero) /// | `"%k"` | `"22"` | Hour of the time in 24 hour clock format, blank-padded ( 0..23) /// | `"%l"` | `"10"` | Hour of the time in 12 hour clock format, blank-padded ( 0..12) /// | `"%L"` | `"000"` | Millisecond of the time (3 digits, left padded with zero) /// | `"%m"` | `"09"` | Month of the time /// | `"%M"` | `"49"` | Minutes of the time (2 digits, left padded with zero e.g 01 02) /// | `"%n"` | | Newline character (\n) /// | `"%N"` | `"000000000"` | Nanoseconds of the time (9 digits, left padded with zero) /// | `"%p"` | `"PM"` | Gives AM / PM of the time /// | `"%P"` | `"pm"` | Gives am / pm of the time /// | `"%r"` | `"10:49:27 PM"` | Long time in 12 hour clock format (%I:%M:%S %p) /// | `"%R"` | `"22:49"` | Short time in 24 hour clock format (%H:%M) /// | `"%s"` | | Number of seconds since 1970-01-01 00:00:00 +0000 /// | `"%S"` | `"27"` | Seconds of the time /// | `"%t"` | | Tab character (\t) /// | `"%T"` | `"22:49:27"` | Long time in 24 hour clock format (%H:%M:%S) /// | `"%u"` | `"4"` | Day of week of the time (from 1 for Monday to 7 for Sunday) /// | `"%U"` | `"36"` | Week number of the current year, starting with the first Sunday as the first day of the first week (00..53) /// | `"%v"` | `"12-SEP-2013"` | VMS date (%e-%b-%Y) (culture invariant) /// | `"%V"` | `"37"` | Week number of the current year according to ISO 8601 (01..53) /// | `"%W"` | `"36"` | Week number of the current year, starting with the first Monday as the first day of the first week (00..53) /// | `"%w"` | `"4"` | Day of week of the time (from 0 for Sunday to 6 for Saturday) /// | `"%x"` | | Preferred representation for the date alone, no time /// | `"%X"` | | Preferred representation for the time alone, no date /// | `"%y"` | `"13"` | Gives year without century of the time /// | `"%Y"` | `"2013"` | Year of the time /// | `"%Z"` | `"IST"` | Gives Time Zone of the time /// | `"%%"` | `"%"` | Output the character `%` /// /// Note that the format is using a good part of the ruby format ([source](http://apidock.com/ruby/DateTime/strftime)) /// /// The current date, parse the input date or return the date object, depending on use cases. /// /// ```kalk /// >>> today = date /// # today = date /// today = 11/22/20 10:13:00 /// >>> today.year /// # today.year /// out = 2_020 /// >>> today.month /// # today.month /// out = 11 /// >>> "30 Nov 2020" |> date /// # "30 Nov 2020" |> date /// out = 11/30/20 00:00:00 /// >>> out |> date.add_days 4 /// # out |> date.add_days(4) /// out = 12/04/20 00:00:00 /// >>> date.format = "%F" /// >>> date /// # date /// out = 2020-11-22 /// ``` /// [KalkExport("date", CategoryMisc)] public object Date(string date = null) { if (!string.IsNullOrEmpty(date)) { return DateTimeFunctions.Parse(Engine, date); } // If we are used in the context an expression that doesn't use a member of the date object // then return the current date var parentNode = Engine.CurrentNode.Parent; if (parentNode != null && !(parentNode is ScriptMemberExpression || parentNode is ScriptIndexerExpression)) { // In testing, we return a fake method return Engine.IsTesting ? new DateTime(2020, 11, 22, 10, 13, 00) : DateTimeFunctions.Now(); } return DateObject; } /// /// Prints the ascii table or convert an input string to an ascii array, or an ascii array to a string. /// /// An optional input (string or array of numbers or directly an integer). /// Depending on the input: /// - If no input, it will display the ascii table /// - If the input is an integer, it will convert it to the equivalent ascii char. /// - If the input is a string, it will convert the string to a byte buffer containing the corresponding ascii bytes. /// - If the input is an array of integer, it will convert each element to the equivalent ascii char. /// /// /// ```kalk /// >>> ascii 65 /// # ascii(65) /// out = "A" /// >>> ascii 97 /// # ascii(97) /// out = "a" /// >>> ascii "A" /// # ascii("A") /// out = 65 /// >>> ascii "kalk" /// # ascii("kalk") /// out = bytebuffer([107, 97, 108, 107]) /// >>> ascii out /// # ascii(out) /// out = "kalk" /// ``` /// [KalkExport("ascii", CategoryMisc)] public object Ascii(object obj = null) { if (obj == null && Engine.CurrentNode.Parent is ScriptExpressionStatement) { var builder = new StringBuilder(); const int alignControls = -38; const int alignStandard = 13; const int columnWidth = 3 + 4 + 1; for (int y = 0; y < 32; y++) { builder.Length = 0; for (int x = 0; x < 8; x++) { var c = x * 32 + y; if (x > 0) builder.Append(" "); var index = $"{c,3}"; var valueAsString = StringFunctions.Escape(ConvertAscii(c)); if (char.GetUnicodeCategory(valueAsString[0]) == UnicodeCategory.Control) { valueAsString = "?"; } var strValue = $"\"{valueAsString}\""; var column = x == 0 ? $"{index} {strValue,-6} {$"({AsciiSpecialCodes[y]})",-27}" : $"{index} {strValue,-4}"; OutputColumn(builder, x, column); } if (y == 0) { Engine.WriteHighlightLine($" {"ASCII controls",alignControls} {"ASCII printable characters",-(columnWidth * 2 + alignStandard + 1)} {"Extended ASCII Characters"}"); } Engine.WriteHighlightLine(builder.ToString()); } void OutputColumn(StringBuilder output, int columnIndex, string text) { output.Append(columnIndex == 0 ? $"{text,-alignControls}" : columnIndex == 3 ? $"{text,-alignStandard}" : $"{text}"); } return null; } // Otherwise convert the argument. return ConvertAscii(Engine, obj); } /// /// Process each element of the input array with the specified function. /// /// A list of element to process. /// A reference to a function that takes 1 parameters and return a value. The function must be passed via the prefix @ to pass a function pointer. /// The value transformed. /// /// ```kalk /// >>> foreach([1, 2, 3, 4], @hex) /// # foreach([1, 2, 3, 4], @hex) /// out = ["01", "02", "03", "04"] /// ``` /// [KalkExport("foreach", CategoryMisc)] public object Foreach(TemplateContext context, IEnumerable list, IScriptCustomFunction func) { if (list is null) return null; if (func is null) return list; var array = new ScriptArray(); var args = new ScriptArray(1); foreach (var item in list) { args[0] = item; var result = ScriptFunctionCall.Call(context, context.CurrentNode, func, args); array.Add(result); } return array; } /// /// Returns the keys of the specified object. /// /// An object to get the keys from. /// The keys of the parameter obj. /// /// ```kalk /// >>> obj = {m: 1, n: 2}; keys obj /// # obj = {m: 1, n: 2}; keys(obj) /// obj = {m: 1, n: 2} /// out = ["m", "n"] /// ``` /// [KalkExport("keys", CategoryMisc)] public IEnumerable Keys(object obj) { return ObjectFunctions.Keys(Engine, obj); } /// /// Returns a new GUID as a string. /// /// A new GUID as a string. /// /// ```kalk /// >>> guid /// # guid /// out = "0deafe30-de4d-47c3-9631-2d3292afbb8e" /// ``` /// [KalkExport("guid", CategoryMisc)] public string Guid() { return Engine.IsTesting ? "0deafe30-de4d-47c3-9631-2d3292afbb8e" : System.Guid.NewGuid().ToString(); } /// /// Returns the size of the specified object. /// /// The object value. /// The size of the object. /// /// ```kalk /// >>> size 1 /// # size(1) /// out = 0 /// >>> size "kalk" /// # size("kalk") /// out = 4 /// >>> size float4(1,2,3,4) /// # size(float4(1, 2, 3, 4)) /// out = 4 /// >>> size [1, 2, 3] /// # size([1, 2, 3]) /// out = 3 /// ``` /// [KalkExport("size", CategoryMisc)] public int Size(object obj) { return ObjectFunctions.Size(obj); } /// /// Returns the values of the specified object. /// /// An object to get the values from. /// The values of the parameter obj. /// /// ```kalk /// >>> obj = {m: 1, n: 2}; values obj /// # obj = {m: 1, n: 2}; values(obj) /// obj = {m: 1, n: 2} /// out = [1, 2] /// ``` /// [KalkExport("values", CategoryMisc)] public IEnumerable Values(TemplateContext context, object obj) { switch (obj) { case IDictionary dict: return ObjectFunctions.Values(context, dict); case IEnumerable list: return new ScriptArray(list); default: return new ScriptArray() {obj}; } } /// /// Converts an integral/bytebuffer input to an hexadecimal representation or convert an hexadecimal input string /// to an integral/bytebuffer representation. /// /// The input value. /// Output the prefix `0x` in front of each hexadecimal bytes when converting /// from integral to hexadecimal. /// The character used to separate hexadecimal bytes when converting /// from integral to hexadecimal. /// The hexadecimal representation of the input or convert the hexadecimal input string /// to an integral representation. /// When converting from a hexadecimal string to an integral representation, this method /// will skip any white-space characters, comma `,`, colon `:`, semi-colon `;`, underscore `_` and /// dash `-`. /// When the hexadecimal input string can be converted to an integral less than or equal 8 bytes (64 bits) /// it will convert it to a single integral result, otherwise it will convert to a bytebuffer. /// See the following examples. /// /// /// ```kalk /// >>> hex 10 /// # hex(10) /// out = "0A" /// >>> hex "12c" /// # hex("12c") /// out = 300 /// >>> hex "0a" /// # hex("0a") /// out = 10 /// >>> hex "0xff030201" /// # hex("0xff030201") /// out = 4_278_387_201 /// >>> hex out /// # hex(out) /// out = "01 02 03 FF" /// >>> hex "01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F" /// # hex("01:02:03:04:05:06:07:08:09:0A:0B:0C:0D:0E:0F") /// out = bytebuffer([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) /// >>> hex(out, true, ",") /// # hex(out, true, ",") /// out = "0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F" /// >>> hex out /// # hex(out) /// out = bytebuffer([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) /// >>> hex("1a,2b;3c 4d-5e_6f") /// # hex("1a,2b;3c 4d-5e_6f") /// out = 103_832_130_169_626 /// >>> hex out /// # hex(out) /// out = "1A 2B 3C 4D 6F 5E 00 00" /// >>> hex float4(1,2,3,4) /// # hex(float4(1, 2, 3, 4)) /// out = "00 00 80 3F 00 00 00 40 00 00 40 40 00 00 80 40" /// ``` /// /// /// ```kalk /// >>> hex short(12345) /// # hex(short(12345)) /// out = "39 30" /// >>> hex int (12345789) /// # hex(int(12345789)) /// out = "BD 61 BC 00" /// ``` /// [KalkExport("hex", CategoryMisc)] public object Hexadecimal(object value, bool prefix = false, string separator = " ") { return Hexadecimal(value, prefix, separator, false); } /// /// Converts an integral/bytebuffer input to a binary representation or convert a binary input string /// to an integral/bytebuffer representation. /// /// The input value. /// Output the prefix `0x` in front of each binary bytes when converting /// from integral to binary. /// The character used to separate binary bytes when converting /// from integral to binary. /// The binary representation of the input or convert the binary input string /// to an integral representation. /// When converting from a binary string to an integral representation, this method /// will skip any white-space characters, comma `,`, colon `:`, semi-colon `;`, underscore `_` and /// dash `-`. /// When the binary input string can be converted to an integral less than or equal 8 bytes (64 bits) /// it will convert it to a single integral result, otherwise it will convert to a bytebuffer. /// See the following examples. /// /// /// ```kalk /// >>> bin 13 /// # bin(13) /// out = "00001101 00000000 00000000 00000000" /// >>> bin out /// # bin(out) /// out = 13 /// >>> bin "111111111011" /// # bin("111111111011") /// out = 4_091 /// >>> bin 0xff030201 /// # bin(-16580095) /// out = "00000001 00000010 00000011 11111111" /// >>> bin out /// # bin(out) /// out = 4_278_387_201 /// >>> bin "11111111000000110000001000000001" /// # bin("11111111000000110000001000000001") /// out = 4_278_387_201 /// >>> bin(byte(5)) /// # bin(byte(5)) /// out = "00000101" /// >>> bin(long(6)) /// # bin(long(6)) /// out = "00000110 00000000 00000000 00000000 00000000 00000000 00000000 00000000" /// >>> bin(out) /// # bin(out) /// out = 6 /// >>> kind(out) /// # kind(out) /// out = "long" /// ``` /// [KalkExport("bin", CategoryMisc)] public object Binary(object value, bool prefix = false, string separator = " ") { return Binary(value, prefix, separator, false); } /// /// Converts a string to an UTF8 bytebuffer or convert a bytebuffer of UTF8 bytes to a string. /// /// The specified input. /// The UTF8 bytebuffer representation of the input string or the string representation of the input UTF8 bytebuffer. /// /// ```kalk /// >>> utf8 "kalk" /// # utf8("kalk") /// out = bytebuffer([107, 97, 108, 107]) /// >>> utf8 out /// # utf8(out) /// out = "kalk" /// ``` /// [KalkExport("utf8", CategoryMisc)] public object GetUtf8(object value) { switch (value) { case string str: { var buffer = Encoding.UTF8.GetBytes(str); return KalkNativeBuffer.AsBytes(buffer.Length, in buffer[0]); } case IEnumerable it: { var bytes = new MemoryStream(); foreach (var b in it) { bytes.WriteByte(Engine.ToObject(0, b)); } return Encoding.UTF8.GetString(bytes.GetBuffer(), 0, (int) bytes.Length); } default: throw new ArgumentException($"The type {Engine.GetTypeName(value)} is not supported ", nameof(value)); } } /// /// Converts a string to an UTF16 bytebuffer or convert a bytebuffer of UTF16 bytes to a string. /// /// The specified input. /// The UTF16 bytebuffer representation of the input string or the string representation of the input UTF16 bytebuffer. /// /// ```kalk /// >>> utf16 "kalk" /// # utf16("kalk") /// out = bytebuffer([107, 0, 97, 0, 108, 0, 107, 0]) /// >>> utf16 out /// # utf16(out) /// out = "kalk" /// ``` /// [KalkExport("utf16", CategoryMisc)] public object GetUtf16(object value) { switch (value) { case string str: { unsafe { fixed (void* pBuffer = str) { return KalkNativeBuffer.AsBytes(str.Length * 2, in *(byte*)pBuffer); } } } case IEnumerable it: { var bytes = new MemoryStream(); foreach (var b in it) { bytes.WriteByte(Engine.ToObject(0, b)); } unsafe { fixed (void* pBuffer = bytes.GetBuffer()) return new string((char*) pBuffer, 0, (int) bytes.Length / 2); } } default: throw new ArgumentException($"The type {Engine.GetTypeName(value)} is not supported ", nameof(value)); } } /// /// Converts a string to an UTF32 bytebuffer or convert a bytebuffer of UTF32 bytes to a string. /// /// The specified input. /// The UTF32 bytebuffer representation of the input string or the string representation of the input UTF32 bytebuffer. /// /// ```kalk /// >>> utf32 "kalk" /// # utf32("kalk") /// out = bytebuffer([107, 0, 0, 0, 97, 0, 0, 0, 108, 0, 0, 0, 107, 0, 0, 0]) /// >>> utf32 out /// # utf32(out) /// out = "kalk" /// ``` /// [KalkExport("utf32", CategoryMisc)] public object GetUtf32(object value) { switch (value) { case string str: { var buffer = Encoding.UTF32.GetBytes(str); return KalkNativeBuffer.AsBytes(buffer.Length, in buffer[0]); } case IEnumerable it: { var bytes = new MemoryStream(); foreach (var b in it) { bytes.WriteByte(Engine.ToObject(0, b)); } return Encoding.UTF32.GetString(bytes.GetBuffer(), 0, (int)bytes.Length); } default: throw new ArgumentException($"The type {Engine.GetTypeName(value)} is not supported ", nameof(value)); } } /// /// Inserts an item into a string or list at the specified index. /// /// A string or list to insert an item into. /// The index at which to insert the item. /// The item to insert. /// A new string with the item inserted, or a new list with the item inserted at the specified index. /// The index is adjusted at the modulo of the length of the input value. /// If the index is < 0, then the index starts from the end of the string/list length + 1. A value of -1 for the index would insert the item at the end, after the last element of the string or list. /// /// /// ```kalk /// >>> insert_at("kalk", 0, "YES") /// # insert_at("kalk", 0, "YES") /// out = "YESkalk" /// >>> insert_at("kalk", -1, "YES") /// # insert_at("kalk", -1, "YES") /// out = "kalkYES" /// >>> insert_at(0..10, 1, 50) /// # insert_at(0..10, 1, 50) /// out = [0, 50, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] /// >>> insert_at(0..9, 21, 50) # final index is 21 % 10 = 1 /// # insert_at(0..9, 21, 50) # final index is 21 % 10 = 1 /// out = [0, 50, 1, 2, 3, 4, 5, 6, 7, 8, 9] /// >>> insert_at([], 3, 1) /// # insert_at([], 3, 1) /// out = [1] /// ``` /// [KalkExport("insert_at", CategoryMisc)] public object InsertAt(object list, int index, object item) { if (list == null) throw new ArgumentNullException(nameof(list)); if (item == null) throw new ArgumentNullException(nameof(item)); if (list is string valueStr) { var itemStr = Engine.ObjectToString(item); index = valueStr.Length == 0 ? 0 : index < 0 ? valueStr.Length + (index + 1) % valueStr.Length : index % valueStr.Length; var builder = new StringBuilder(valueStr.Substring(0, index)); builder.Append(itemStr); if (index < valueStr.Length) { builder.Append(valueStr.Substring(index)); } return builder.ToString(); } else if (list is KalkNativeBuffer buffer) { var byteItem = Engine.ToObject(2, item); index = buffer.Count == 0 ? 0 : index < 0 ? buffer.Count + (index + 1) % buffer.Count : index % buffer.Count; var newBuffer = new KalkNativeBuffer(buffer.Count + 1); for (int i = 0; i < index; i++) newBuffer[i] = buffer[i]; newBuffer[index] = byteItem; for (int i = index + 1; i < newBuffer.Count; i++) newBuffer[i] = buffer[i - 1]; return newBuffer; } else if (list is IList listCollection) { index = listCollection.Count == 0 ? 0 : index < 0 ? listCollection.Count + (index + 1) % listCollection.Count : index % listCollection.Count; listCollection.Insert(index, item); return listCollection; } else { throw new ArgumentException($"The type {Engine.GetTypeName(list)} is not supported ", nameof(list)); } } /// /// Removes an item from a string or list at the specified index. /// /// A string or list to remove an item from. /// The index at which to remove the item. /// A new string/list with the item at the specified index removed. /// The index is adjusted at the modulo of the length of the input value. /// If the index is < 0, then the index starts from the end of the string/list length. A value of -1 for the index would remove the last element. /// /// /// ```kalk /// >>> remove_at("kalk", 0) /// # remove_at("kalk", 0) /// out = "alk" /// >>> remove_at("kalk", -1) /// # remove_at("kalk", -1) /// out = "kal" /// >>> remove_at(0..9, 5) /// # remove_at(0..9, 5) /// out = [0, 1, 2, 3, 4, 6, 7, 8, 9] /// >>> remove_at(0..9, -1) /// # remove_at(0..9, -1) /// out = [0, 1, 2, 3, 4, 5, 6, 7, 8] /// >>> remove_at(asbytes(0x04030201), 1) /// # remove_at(asbytes(67305985), 1) /// out = bytebuffer([1, 3, 4]) /// ``` /// [KalkExport("remove_at", CategoryMisc)] public object RemoveAt(object list, int index) { if (list == null) throw new ArgumentNullException(nameof(list)); if (list is string valueStr) { if (valueStr == string.Empty) return valueStr; index = index < 0 ? valueStr.Length + index % valueStr.Length : index % valueStr.Length; var builder = new StringBuilder(valueStr.Substring(0, index)); if (index < valueStr.Length) { builder.Append(valueStr.Substring(index + 1)); } return builder.ToString(); } else if (list is KalkNativeBuffer buffer) { if (buffer.Count == 0) return buffer; index = index < 0 ? buffer.Count + index % buffer.Count : index % buffer.Count; var newBuffer = new KalkNativeBuffer(buffer.Count - 1); for (int i = 0; i < index; i++) newBuffer[i] = buffer[i]; for (int i = index; i < newBuffer.Count; i++) newBuffer[i] = buffer[i + 1]; return newBuffer; } else if (list is IList listCollection) { if (listCollection.Count == 0) return listCollection; index = index < 0 ? listCollection.Count + index % listCollection.Count : index % listCollection.Count; listCollection.RemoveAt(index); return listCollection; } else { throw new ArgumentException($"The type {Engine.GetTypeName(list)} is not supported ", nameof(list)); } } /// /// Checks if an object (string, list, vector types, bytebuffer...) is containing the specified value. /// /// The list to search into. /// The value to search into the list. /// true if value was found in the list input; otherwise false. /// /// ```kalk /// >>> contains("kalk", "l") /// # contains("kalk", "l") /// out = true /// >>> contains("kalk", "e") /// # contains("kalk", "e") /// out = false /// >>> contains([1,2,3,4,5], 3) /// # contains([1,2,3,4,5], 3) /// out = true /// >>> contains([1,2,3,4,5], 6) /// # contains([1,2,3,4,5], 6) /// out = false /// >>> contains(float4(1,2,3,4), 3) /// # contains(float4(1, 2, 3, 4), 3) /// out = true /// >>> contains(float4(1,2,3,4), 6) /// # contains(float4(1, 2, 3, 4), 6) /// out = false /// ``` /// [KalkExport("contains", CategoryMisc)] public KalkBool Contains(object list, object value) { if (list == null) throw new ArgumentNullException(nameof(list)); if (value == null) throw new ArgumentNullException(nameof(value)); if (list is string valueStr) { var matchStr = Engine.ObjectToString(value); return valueStr.Contains(matchStr, StringComparison.Ordinal); } var composite = new KalkCompositeValue(list); bool contains = false; // Force the evaluation of the object composite.Visit(Engine, Engine.CurrentSpan, input => { var result = (bool) ScriptBinaryExpression.Evaluate(Engine, Engine.CurrentSpan, ScriptBinaryOperator.CompareEqual, input, value); if (result) { contains = true; return false; } return true; }); return contains; } /// /// Reverse a list of values. /// /// The list to reverse. /// The list of values reversed. /// /// ```kalk /// >>> reverse([1,2,3,4,5]) /// # reverse([1,2,3,4,5]) /// out = [5, 4, 3, 2, 1] /// >>> reverse("abc") /// # reverse("abc") /// out = "cba" /// >>> reverse(asbytes(0x04030201)) /// # reverse(asbytes(67305985)) /// out = bytebuffer([4, 3, 2, 1]) /// ``` /// remove_at(asbytes(0x04030201), 1) [KalkExport("reverse", CategoryMisc)] public object Reverse(object list) { if (list is string valueStr) { var builder = new StringBuilder(valueStr.Length); for (var i = valueStr.Length - 1; i >= 0 ; i--) { var c = valueStr[i]; builder.Append(c); } return builder.ToString(); } if (list is KalkNativeBuffer nativeBuffer) { var reversedNativeBuffer = new KalkNativeBuffer(nativeBuffer.Count); int j = 0; for (int i = nativeBuffer.Count - 1; i >= 0; i--) { reversedNativeBuffer[j++] = nativeBuffer[i]; } return reversedNativeBuffer; } if (list is IEnumerable it) { var array = new ScriptArray(); foreach (var b in it.Cast().Reverse()) { array.Add(b); } return array; } return list; } /// /// Replaces in an object (string, list, vector types, bytebuffer...) an item of the specified value by another value. /// /// The list to search into to replace an element. /// The item to replace. /// The value to replace with. /// The modified object. /// /// ```kalk /// >>> replace("kalk", "k", "woo") /// # replace("kalk", "k", "woo") /// out = "wooalwoo" /// >>> replace([1,2,3,4], 3, 5) /// # replace([1,2,3,4], 3, 5) /// out = [1, 2, 5, 4] /// >>> replace(float4(1,2,3,4), 3, 5) /// # replace(float4(1, 2, 3, 4), 3, 5) /// out = float4(1, 2, 5, 4) /// ``` /// [KalkExport("replace", CategoryMisc)] public object Replace(object list, object value, object by) { if (list == null) throw new ArgumentNullException(nameof(list)); if (value == null) throw new ArgumentNullException(nameof(value)); if (@by == null) throw new ArgumentNullException(nameof(@by)); if (list is string valueStr) { var matchStr = Engine.ObjectToString(value); var byStr = Engine.ObjectToString(by); return valueStr.Replace(matchStr, byStr); } var composite = new KalkCompositeValue(list); return composite.Transform(Engine, Engine.CurrentSpan, input => ReplaceImpl(input, value, @by), typeof(object)); } /// /// Creates a slice of an object (string, list, vector types, bytebuffer...) starting at the specified index and with the specified length; /// /// The object to create a slice from. /// The index into the object. /// The optional length of the slice. If the length is not defined, the length will start from index with the remaining elements. /// A slice of the input object. /// The index is adjusted at the modulo of the specified length of the input object. /// If the index is < 0, then the index starts from the end of the input object length. A value of -1 for the index would take a slice with the only the last element. /// /// /// ```kalk /// >>> slice("kalk", 1) /// # slice("kalk", 1) /// out = "alk" /// >>> slice("kalk", -2) /// # slice("kalk", -2) /// out = "lk" /// >>> slice("kalk", 1, 2) /// # slice("kalk", 1, 2) /// out = "al" /// >>> slice([1,2,3,4], 1) /// # slice([1,2,3,4], 1) /// out = [2, 3, 4] /// >>> slice([1,2,3,4], -1) /// # slice([1,2,3,4], -1) /// out = [4] /// >>> slice([1,2,3,4], -1, 3) # length is bigger than expected, no errors /// # slice([1,2,3,4], -1, 3) # length is bigger than expected, no errors /// out = [4] /// >>> slice(asbytes(0x04030201), 1, 2) /// # slice(asbytes(67305985), 1, 2) /// out = slice(bytebuffer([2, 3]), 1, 2) /// ``` /// [KalkExport("slice", CategoryMisc)] public object Slice(object list, int index, int? length = null) { if (list is string str) { return StringFunctions.Slice(str, index, length); } var listCollection = list as IList; if (listCollection == null) { if (list is IEnumerable it) { listCollection = new ScriptRange(it); } else { throw new ArgumentException("The argument is not a string, bytebuffer or array.", nameof(list)); } } // Wrap index index = index < 0 ? listCollection.Count + index % listCollection.Count : index % listCollection.Count; // Compute length length ??= listCollection.Count; if (length < 0) { throw new ArgumentOutOfRangeException(nameof(length), "Length must be >= 0"); } // Trim length if required if (index + length > listCollection.Count) { length = listCollection.Count - index; } if (list is KalkNativeBuffer nativeBuffer) { return nativeBuffer.Slice(index, length.Value); } return new ScriptRange(listCollection.Cast().Skip(index).Take(length.Value)); } /// /// Extract lines from the specified string. /// /// A string to extract lines from. /// Lines extracted from the input string. /// /// ```kalk /// >>> lines("k\na\nl\nk") /// # lines("k\na\nl\nk") /// out = ["k", "a", "l", "k"] /// ``` /// [KalkExport("lines", CategoryMisc)] public ScriptRange Lines(string text) { if (text == null) return new ScriptRange(); return new ScriptRange(new LineReader(() => new StringReader(text))); } /// /// Display or returns the known CSS colors. /// /// Prints known CSS colors or return a list if this function is used in an expression. /// /// ```kalk /// >>> colors[0] /// # colors[0] /// out = rgb(240, 248, 255) ## F0F8FF AliceBlue ## /// >>> mycolor = colors["AliceBlue"]; mycolor.name /// # mycolor = colors["AliceBlue"]; mycolor.name /// mycolor = rgb(240, 248, 255) ## F0F8FF AliceBlue ## /// out = "AliceBlue" /// ``` /// [KalkExport("colors", CategoryMisc)] public object Colors() { var knownColorsInternal = KalkColorRgb.GetKnownColors(); if (!(Engine?.CurrentNode is ScriptVariable) || !(Engine?.CurrentNode?.Parent is ScriptExpressionStatement)) { var colors = new ScriptArray(knownColorsInternal); foreach (var color in knownColorsInternal) { colors.SetValue(color.Name, color, false); } return colors; } var builder = new StringBuilder(); int count = 0; const int PerColumn = 2; foreach (var knownColor in knownColorsInternal) { var colorName = knownColor.ToString("aligned", Engine); builder.Append(colorName); count++; if (count == PerColumn) { Engine.WriteHighlightLine(builder.ToString()); builder.Clear(); count = 0; } else { builder.Append(" "); } } if (builder.Length > 0) { Engine.WriteHighlightLine(builder.ToString()); builder.Clear(); } return null; } private object ReplaceImpl(object value, object match, object by) { var result = (bool)ScriptBinaryExpression.Evaluate(Engine, Engine.CurrentSpan, ScriptBinaryOperator.CompareEqual, value, match); return result ? @by : value; } private object Hexadecimal(object value, bool prefix, string separator, bool returnString) { Engine.CheckAbort(); switch (value) { case string str when returnString: throw new ArgumentException($"Cannot convert a string to hexadecimal inside a list", nameof(value)); case string str: { var array = new List(); var temp = new List(); int count = 0; bool hasPrefix = false; int hexa = 0; void FlushBytes() { bool isOdd = count > 0; if (isOdd) { temp.Add((byte)(hexa << 4)); } count = 0; if (temp.Count > 0) { if (isOdd) { for (int j = temp.Count - 1; j >= 0; j--) { var hb = j - 1 >= 0 ? (temp[j - 1] & 0xF) << 4 : 0; var lb = temp[j] >> 4; var fullByte = (byte)(hb | lb); array.Add(fullByte); } } else { for (int j = temp.Count - 1; j >= 0; j--) { array.Add(temp[j]); } } temp.Clear(); } } for (int i = 0; i < str.Length; i++) { var c = str[i]; if (char.IsWhiteSpace(c) || c == ',' || c == ':' || c == ';' || c == '_' || c == '-') { if (c != '_') { FlushBytes(); } hasPrefix = c == '_' && hasPrefix; // reset prefix after parsing but not for `_` continue; } // Skip 0x prefix if (!hasPrefix && c == '0' && i + 1 < str.Length && str[i + 1] == 'x') { FlushBytes(); hasPrefix = true; i++; continue; } //if (c >= '0' && c <= '9' || c >= 'A' && c <= 'F' || c ) if (CharHelper.TryHexaToInt(c, out var n)) { // TODO: Add parsing for 0x00 hexa = (hexa << 4) | n; if (count == 1) { temp.Add((byte)hexa); hexa = 0; count = 0; } else { count++; } } else { throw new ArgumentException($"Invalid character found `{StringFunctions.Escape(c.ToString())}`. Expecting only hexadecimal.", nameof(value)); } } FlushBytes(); if (array.Count <= 8) { long ulongValue = 0; unsafe { for (int i = 0; i < array.Count; i++) { ((byte*)&ulongValue)[i] = array[i]; } } return ulongValue; } return new KalkNativeBuffer(array); } case byte vbyte: return HexaFromBytes(1, vbyte, prefix, separator); case sbyte vsbyte: return HexaFromBytes(1, vsbyte, prefix, separator); case short vshort: { int size = 2; if (vshort >= sbyte.MinValue && vshort <= byte.MaxValue) size = 1; return HexaFromBytes(size, vshort, prefix, separator); } case ushort vushort: { int size = 2; if (vushort <= byte.MaxValue) size = 1; return HexaFromBytes(size, vushort, prefix, separator); } case int vint: { int size = 4; if (vint >= sbyte.MinValue && vint <= byte.MaxValue) size = 1; else if (vint >= short.MinValue && vint <= ushort.MaxValue) size = 2; return HexaFromBytes(size, vint, prefix, separator); } case uint vuint: { int size = 4; if (vuint <= byte.MaxValue) size = 1; else if (vuint <= ushort.MaxValue) size = 2; return HexaFromBytes(size, vuint, prefix, separator); } case long vlong: { int size = 8; if (vlong >= sbyte.MinValue && vlong <= byte.MaxValue) size = 1; else if (vlong >= short.MinValue && vlong <= ushort.MaxValue) size = 2; else if (vlong >= int.MinValue && vlong <= uint.MaxValue) size = 4; return HexaFromBytes(size, vlong, prefix, separator); } case ulong vulong: { int size = 8; if (vulong <= byte.MaxValue) size = 1; else if (vulong <= ushort.MaxValue) size = 2; else if (vulong <= uint.MaxValue) size = 4; return HexaFromBytes(size, vulong, prefix, separator); } case float vfloat: { var floatAsInt = BitConverter.SingleToInt32Bits(vfloat); return HexaFromBytes(4, floatAsInt, prefix, separator); } case double vdouble: { var doubleAsLong = BitConverter.DoubleToInt64Bits(vdouble); return HexaFromBytes(8, doubleAsLong, prefix, separator); } case BigInteger bigInt: { var array = bigInt.ToByteArray(); return HexaFromBytes(array.Length, array[0], prefix, separator); } case KalkValue kalkValue: { var span = kalkValue.AsSpan(); return HexaFromBytes(span.Length, span[0], prefix, separator); } case IEnumerable list: { var builder = new StringBuilder(); bool isFirst = true; foreach (var item in list) { if (!isFirst) { builder.Append(separator); } isFirst = false; byte byteItem = Engine.ToObject(0, item); if (prefix) builder.Append("0x"); builder.Append(byteItem.ToString("X2")); } return builder.ToString(); } default: throw new ArgumentException($"The type {Engine.GetTypeName(value)} is not supported ", nameof(value)); } } private static string HexaFromBytes(int byteCount, in T element, bool prefix, string separator) { var builder = new StringBuilder(byteCount * 2); for (int i = 0; i < byteCount; i++) { if (i > 0) builder.Append(separator); var b = Unsafe.As(ref Unsafe.AddByteOffset(ref Unsafe.AsRef(element), new IntPtr(i))); if (prefix) builder.Append("0x"); builder.Append(b.ToString("X2")); } return builder.ToString(); } private object Binary(object value, bool prefix, string separator, bool returnString) { Engine.CheckAbort(); switch (value) { case string str when returnString: throw new ArgumentException($"Cannot convert a string to binary inside a list", nameof(value)); case string str: { var array = new List(); var temp = new List(); int count = 0; bool hasPrefix = false; int bin = 0; void FlushBytes() { bool isOdd = count > 0; var shift = 8 - count; if (count > 0) { temp.Add((byte)(bin << shift)); } bin = 0; if (temp.Count > 0) { if (isOdd) { for (int j = temp.Count - 1; j >= 0; j--) { var hb = j - 1 >= 0 ? (byte)((temp[j - 1]) << shift) : (byte)0; var lb = temp[j] >> count; var fullByte = (byte)(hb | lb); array.Add(fullByte); } } else { for (int j = temp.Count - 1; j >= 0; j--) { array.Add(temp[j]); } } temp.Clear(); } count = 0; } for (int i = 0; i < str.Length; i++) { var c = str[i]; if (char.IsWhiteSpace(c) || c == ',' || c == ':' || c == ';' || c == '_' || c == '-') { if (c != '_') { FlushBytes(); } hasPrefix = c == '_' && hasPrefix; // reset prefix after parsing but not for `_` continue; } // Skip 0b prefix if (!hasPrefix && c == '0' && i + 1 < str.Length && str[i + 1] == 'b') { FlushBytes(); hasPrefix = true; i++; continue; } if (c == '0' || c == '1') { bin = (bin << 1) | (c - '0'); if (count == 7) { temp.Add((byte)bin); bin = 0; count = 0; } else { count++; } } else { throw new ArgumentException($"Invalid character found `{StringFunctions.Escape(c.ToString())}`. Expecting only binary 0 or 1.", nameof(value)); } } FlushBytes(); if (array.Count <= 8) { long ulongValue = 0; unsafe { for (int i = 0; i < array.Count; i++) { ((byte*)&ulongValue)[i] = array[i]; } } return ulongValue; } return new KalkNativeBuffer(array); } case byte vbyte: return BinaryFromBytes(1, vbyte, prefix, separator); case sbyte vsbyte: return BinaryFromBytes(1, vsbyte, prefix, separator); case short vshort: { int size = 2; return BinaryFromBytes(size, vshort, prefix, separator); } case ushort vushort: { int size = 2; return BinaryFromBytes(size, vushort, prefix, separator); } case int vint: { int size = 4; return BinaryFromBytes(size, vint, prefix, separator); } case uint vuint: { int size = 4; return BinaryFromBytes(size, vuint, prefix, separator); } case long vlong: { int size = 8; return BinaryFromBytes(size, vlong, prefix, separator); } case ulong vulong: { int size = 8; return BinaryFromBytes(size, vulong, prefix, separator); } case float vfloat: { var floatAsInt = BitConverter.SingleToInt32Bits(vfloat); return BinaryFromBytes(4, floatAsInt, prefix, separator); } case double vdouble: { var doubleAsLong = BitConverter.DoubleToInt64Bits(vdouble); return BinaryFromBytes(8, doubleAsLong, prefix, separator); } case BigInteger bigInt: { var array = bigInt.ToByteArray(); return BinaryFromBytes(array.Length, array[0], prefix, separator); } case KalkValue kalkValue: { var span = kalkValue.AsSpan(); return BinaryFromBytes(span.Length, span[0], prefix, separator); } case IEnumerable list: { var builder = new StringBuilder(); bool isFirst = true; foreach (var item in list) { if (!isFirst) { builder.Append(separator); } isFirst = false; byte byteItem = Engine.ToObject(0, item); if (prefix) builder.Append("0b"); AppendBinaryByte(builder, byteItem); } return builder.ToString(); } default: throw new ArgumentException($"The type {Engine.GetTypeName(value)} is not supported ", nameof(value)); } } private static void AppendBinaryByte(StringBuilder builder, byte byteItem) { for (int i = 0; i < 8; i++) { var b = (byteItem >> (7 - i)) & 1; builder.Append(b == 1 ? '1' : '0'); } } private static string BinaryFromBytes(int byteCount, in T element, bool prefix, string separator) { var builder = new StringBuilder(byteCount * 8); for (int i = 0; i < byteCount; i++) { if (i > 0) builder.Append(separator); var b = Unsafe.As(ref Unsafe.AddByteOffset(ref Unsafe.AsRef(element), new IntPtr(i))); if (prefix) builder.Append("0b"); AppendBinaryByte(builder, b); } return builder.ToString(); } private static object ConvertAscii(KalkEngine context, object argument) { if (argument is string text) { var bytes = EncodingExtendedAscii.GetBytes(text); if (bytes.Length == 1) { return bytes[0]; } return new KalkNativeBuffer(bytes); } else if (argument is KalkNativeBuffer buffer) { return EncodingExtendedAscii.GetString(buffer.AsSpan()); } else if (argument is IEnumerable it) { return new ScriptRange(ConvertAsciiIteration(context, it)); } else { return ConvertAscii(context.ToInt(context.CurrentSpan, argument)); } } private static IEnumerable ConvertAsciiIteration(KalkEngine context, IEnumerable it) { var iterator = it.GetEnumerator(); while (iterator.MoveNext()) { yield return ConvertAscii(context, iterator.Current); } } private static unsafe string ConvertAscii(int c) { var value = (byte)c; return EncodingExtendedAscii.GetString(&value, 1); } private static readonly string[] AsciiSpecialCodes = new string[32] { "NUL / Null", "SOH / Start of Heading", "STX / Start of Text", "ETX / End of Text", "EOT / End of Transmission", "ENQ / Enquiry", "ACK / Acknowledgment", "BEL / Bell", "BS / Backspace", "HT / Horizontal Tab", "LF / Line Feed", "VT / Vertical Tab", "FF / Form Feed", "CR / Carriage Return", "SO / Shift Out", "SI / Shift In", "DLE / Data Link Escape", "DC1 / Device Control 1", "DC2 / Device Control 2", "DC3 / Device Control 3", "DC4 / Device Control 4", "NAK / Negative Ack", "SYN / Synchronous Idle", "ETB / End of Trans Block", "CAN / Cancel", "EM / End of Medium", "SUB / Substitute", "ESC / Escape", "FS / File Separator", "GS / Group Separator", "RS / Record Separator", "US / Unit Separator", }; } }