| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- 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 plusSign = 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 '-':
- if (leftJustify) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
- leftJustify = true;
- break;
- case '+':
- if (plusSign) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
- plusSign = true;
- break;
- case '0':
- if (zeroPadding) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
- zeroPadding = true;
- break;
- case '#':
- if (alternateForm) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
- alternateForm = true;
- break;
- case ' ':
- if (blank) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
- blank = true;
- break;
- default:
- goto PROCESS_WIDTH;
- }
- i++;
- }
- PROCESS_WIDTH:
- // Process width
- var start = i;
- if (char.IsDigit(format[i]))
- {
- i++;
- if (char.IsDigit(format[i])) i++;
- if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (width or precision too long)");
- width = int.Parse(format.AsSpan()[start..i]);
- }
- // Process precision
- if (format[i] == '.')
- {
- i++;
- start = i;
- if (char.IsDigit(format[i])) i++;
- if (char.IsDigit(format[i])) i++;
- if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (width or precision too long)");
- precision = int.Parse(format.AsSpan()[start..i]);
- }
- // Process conversion specifier
- var specifier = format[i];
- if (context.ArgumentCount <= parameterIndex)
- {
- throw new LuaRuntimeException(context.State.GetTraceback(), $"bad argument #{parameterIndex + 1} to 'format' (no value)");
- }
- 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;
- }
- if (plusSign && f >= 0)
- {
- formattedValue = $"+{formattedValue}";
- }
- break;
- 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 && precision <= formattedValue.Length)
- {
- 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:
- formattedValue = $"\"{StringHelper.Escape(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 'u':
- case 'c':
- 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);
- switch (specifier)
- {
- case 'i':
- case 'd':
- {
- var integer = checked((long)x);
- formattedValue = precision < 0
- ? integer.ToString()
- : integer.ToString($"D{precision}");
- }
- break;
- case 'u':
- {
- var integer = checked((ulong)x);
- formattedValue = precision < 0
- ? integer.ToString()
- : integer.ToString($"D{precision}");
- }
- break;
- case 'c':
- formattedValue = ((char)(int)x).ToString();
- break;
- case 'x':
- {
- var integer = checked((ulong)x);
- formattedValue = alternateForm
- ? $"0x{integer:x}"
- : $"{integer:x}";
- }
- break;
- case 'X':
- {
- var integer = checked((ulong)x);
- formattedValue = alternateForm
- ? $"0X{integer:X}"
- : $"{integer:X}";
- }
- break;
- case 'o':
- {
- var integer = checked((long)x);
- formattedValue = Convert.ToString(integer, 8);
- }
- break;
- }
- if (plusSign && x >= 0)
- {
- formattedValue = $"+{formattedValue}";
- }
- 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;
- }
- }
|