Browse Source

Add: string.format

AnnulusGames 1 year ago
parent
commit
d6da9da644
2 changed files with 249 additions and 0 deletions
  1. 1 0
      src/Lua/Standard/OpenLibExtensions.cs
  2. 248 0
      src/Lua/Standard/Text/FormatFunction.cs

+ 1 - 0
src/Lua/Standard/OpenLibExtensions.cs

@@ -80,6 +80,7 @@ public static class OpenLibExtensions
         ByteFunction.Instance,
         CharFunction.Instance,
         DumpFunction.Instance,
+        FormatFunction.Instance,
         LenFunction.Instance,
         LowerFunction.Instance,
         RepFunction.Instance,

+ 248 - 0
src/Lua/Standard/Text/FormatFunction.cs

@@ -0,0 +1,248 @@
+
+using System.Text;
+using Lua.Internal;
+
+namespace Lua.Standard.Text;
+
+// Ignore 'p' format
+ 
+public sealed class FormatFunction : LuaFunction
+{
+    public override string Name => "format";
+    public static readonly FormatFunction Instance = new();
+
+    protected override async ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
+    {
+        var format = context.GetArgument<string>(0);
+
+        // TODO: pooling StringBuilder
+        var builder = new StringBuilder(format.Length * 2);
+        var parameterIndex = 1;
+
+        for (int i = 0; i < format.Length; i++)
+        {
+            if (format[i] == '%')
+            {
+                i++;
+
+                // escape
+                if (format[i] == '%')
+                {
+                    builder.Append('%');
+                    continue;
+                }
+                
+                var leftJustify = false;
+                var zeroPadding = false;
+                var alternateForm = false;
+                var blank = false;
+                var width = 0;
+                var precision = -1;
+
+                // Process flags
+                while (true)
+                {
+                    var c = format[i];
+                    switch (c)
+                    {
+                        case '-':
+                            leftJustify = true;
+                            break;
+                        case '0':
+                            zeroPadding = true;
+                            break;
+                        case '#':
+                            alternateForm = true;
+                            break;
+                        case ' ':
+                            blank = true;
+                            break;
+                        default:
+                            goto PROCESS_WIDTH;
+                    }
+
+                    i++;
+                }
+
+                PROCESS_WIDTH:
+
+                // Process width
+                if (char.IsDigit(format[i]))
+                {
+                    var start = i;
+
+                    do i++;
+                    while (format.Length > i && char.IsDigit(format[i]));
+
+                    width = int.Parse(format.AsSpan()[start..i]);
+                }
+
+                // Process precision
+                if (format[i] == '.')
+                {
+                    i++;
+                    var start = i;
+
+                    do i++;
+                    while (format.Length > i && char.IsDigit(format[i]));
+
+                    precision = int.Parse(format.AsSpan()[start..i]);
+                }
+
+                // Process conversion specifier
+                var specifier = format[i];
+                var parameter = context.GetArgument(parameterIndex++);
+
+                // TODO: reduce allocation
+                string formattedValue = default!;
+                switch (specifier)
+                {
+                    case 'f':
+                    case 'e':
+                    case 'g':
+                    case 'G':
+                        if (!parameter.TryRead<double>(out var f))
+                        {
+                            LuaRuntimeException.BadArgument(context.State.GetTraceback(), parameterIndex + 1, "format", LuaValueType.Number.ToString(), parameter.Type.ToString());
+                        }
+
+                        switch (specifier)
+                        {
+                            case 'f':
+                                formattedValue = precision < 0
+                                    ? f.ToString()
+                                    : f.ToString($"F{precision}");
+                                break;
+                            case 'e':
+                                formattedValue = precision < 0
+                                    ? f.ToString()
+                                    : f.ToString($"E{precision}");
+                                break;
+                            case 'g':
+                                formattedValue = precision < 0
+                                    ? f.ToString()
+                                    : f.ToString($"G{precision}");
+                                break;
+                            case 'G':
+                                formattedValue = precision < 0
+                                    ? f.ToString().ToUpper()
+                                    : f.ToString($"G{precision}").ToUpper();
+                                break;
+                        }
+
+                        break;
+                    case 'c':
+                    case 's':
+                        using (var strBuffer = new PooledArray<LuaValue>(1))
+                        {
+                            await parameter.CallToStringAsync(context, strBuffer.AsMemory(), cancellationToken);
+                            formattedValue = strBuffer[0].Read<string>();
+                        }
+
+                        if (specifier is 's' && precision > 0)
+                        {
+                            formattedValue = formattedValue[..precision];
+                        }
+                        break;
+                    case 'q':
+                        switch (parameter.Type)
+                        {
+                            case LuaValueType.Nil:
+                                formattedValue = "nil";
+                                break;
+                            case LuaValueType.Boolean:
+                                formattedValue = parameter.Read<bool>() ? "true" : "false";
+                                break;
+                            case LuaValueType.String:
+                                // TODO: support escape sequence
+                                formattedValue = parameter.Read<string>();
+                                break;
+                            case LuaValueType.Number:
+                                // TODO: floating point numbers must be in hexadecimal notation
+                                formattedValue = parameter.Read<double>().ToString();
+                                break;
+                            default:
+                                using (var strBuffer = new PooledArray<LuaValue>(1))
+                                {
+                                    await parameter.CallToStringAsync(context, strBuffer.AsMemory(), cancellationToken);
+                                    formattedValue = strBuffer[0].Read<string>();
+                                }
+                                break;
+                        }
+                        break;
+                    case 'i':
+                    case 'd':
+                    case 'x':
+                    case 'X':
+                        if (!parameter.TryRead<double>(out var x))
+                        {
+                            LuaRuntimeException.BadArgument(context.State.GetTraceback(), parameterIndex + 1, "format", LuaValueType.Number.ToString(), parameter.Type.ToString());
+                        }
+
+                        LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, parameterIndex + 1, x);
+
+                        var integer = (long)x;
+
+                        switch (specifier)
+                        {
+                            case 'i':
+                            case 'd':
+                            case 'u':
+                                formattedValue = precision < 0 
+                                    ? integer.ToString() 
+                                    : integer.ToString($"D{precision}");
+                                break;
+                            case 'x':
+                                formattedValue = alternateForm
+                                    ? $"0x{integer:x}"
+                                    : $"{integer:x}";
+                                break;
+                            case 'X':
+                                formattedValue = alternateForm
+                                    ? $"0X{integer:X}"
+                                    : $"{integer:X}";
+                                break;
+                            case 'o':
+                                formattedValue = Convert.ToString(integer, 8);
+                                break;
+                        }
+                        break;
+                    default:
+                        throw new LuaRuntimeException(context.State.GetTraceback(), $"invalid option '%{specifier}' to 'format'");
+                }
+
+                // Apply blank (' ') flag for positive numbers
+                if (specifier is 'd' or 'i' or 'f' or 'g' or 'G')
+                {
+                    if (blank && !leftJustify && !zeroPadding && parameter.Read<double>() >= 0)
+                    {
+                        formattedValue = $" {formattedValue}";
+                    }
+                }
+
+                // Apply width and padding
+                if (width > formattedValue.Length)
+                {
+                    if (leftJustify)
+                    {
+                        formattedValue = formattedValue.PadRight(width);
+                    }
+                    else
+                    {
+                        formattedValue = zeroPadding ? formattedValue.PadLeft(width, '0') : formattedValue.PadLeft(width);
+                    }
+                }
+
+                builder.Append(formattedValue);
+            }
+            else
+            {
+                builder.Append(format[i]);
+            }
+        }
+
+
+        buffer.Span[0] = builder.ToString();
+        return 1;
+    }
+}