FormatFunction.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. using System.Text;
  2. using Lua.Internal;
  3. namespace Lua.Standard.Text;
  4. // Ignore 'p' format
  5. public sealed class FormatFunction : LuaFunction
  6. {
  7. public override string Name => "format";
  8. public static readonly FormatFunction Instance = new();
  9. protected override async ValueTask<int> InvokeAsyncCore(LuaFunctionExecutionContext context, Memory<LuaValue> buffer, CancellationToken cancellationToken)
  10. {
  11. var format = context.GetArgument<string>(0);
  12. // TODO: pooling StringBuilder
  13. var builder = new StringBuilder(format.Length * 2);
  14. var parameterIndex = 1;
  15. for (int i = 0; i < format.Length; i++)
  16. {
  17. if (format[i] == '%')
  18. {
  19. i++;
  20. // escape
  21. if (format[i] == '%')
  22. {
  23. builder.Append('%');
  24. continue;
  25. }
  26. var leftJustify = false;
  27. var plusSign = false;
  28. var zeroPadding = false;
  29. var alternateForm = false;
  30. var blank = false;
  31. var width = 0;
  32. var precision = -1;
  33. // Process flags
  34. while (true)
  35. {
  36. var c = format[i];
  37. switch (c)
  38. {
  39. case '-':
  40. if (leftJustify) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  41. leftJustify = true;
  42. break;
  43. case '+':
  44. if (plusSign) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  45. plusSign = true;
  46. break;
  47. case '0':
  48. if (zeroPadding) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  49. zeroPadding = true;
  50. break;
  51. case '#':
  52. if (alternateForm) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  53. alternateForm = true;
  54. break;
  55. case ' ':
  56. if (blank) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (repeated flags)");
  57. blank = true;
  58. break;
  59. default:
  60. goto PROCESS_WIDTH;
  61. }
  62. i++;
  63. }
  64. PROCESS_WIDTH:
  65. // Process width
  66. var start = i;
  67. if (char.IsDigit(format[i]))
  68. {
  69. i++;
  70. if (char.IsDigit(format[i])) i++;
  71. if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (width or precision too long)");
  72. width = int.Parse(format.AsSpan()[start..i]);
  73. }
  74. // Process precision
  75. if (format[i] == '.')
  76. {
  77. i++;
  78. start = i;
  79. if (char.IsDigit(format[i])) i++;
  80. if (char.IsDigit(format[i])) i++;
  81. if (char.IsDigit(format[i])) throw new LuaRuntimeException(context.State.GetTraceback(), "invalid format (width or precision too long)");
  82. precision = int.Parse(format.AsSpan()[start..i]);
  83. }
  84. // Process conversion specifier
  85. var specifier = format[i];
  86. if (context.ArgumentCount <= parameterIndex)
  87. {
  88. throw new LuaRuntimeException(context.State.GetTraceback(), $"bad argument #{parameterIndex + 1} to 'format' (no value)");
  89. }
  90. var parameter = context.GetArgument(parameterIndex++);
  91. // TODO: reduce allocation
  92. string formattedValue = default!;
  93. switch (specifier)
  94. {
  95. case 'f':
  96. case 'e':
  97. case 'g':
  98. case 'G':
  99. if (!parameter.TryRead<double>(out var f))
  100. {
  101. LuaRuntimeException.BadArgument(context.State.GetTraceback(), parameterIndex + 1, "format", LuaValueType.Number.ToString(), parameter.Type.ToString());
  102. }
  103. switch (specifier)
  104. {
  105. case 'f':
  106. formattedValue = precision < 0
  107. ? f.ToString()
  108. : f.ToString($"F{precision}");
  109. break;
  110. case 'e':
  111. formattedValue = precision < 0
  112. ? f.ToString()
  113. : f.ToString($"E{precision}");
  114. break;
  115. case 'g':
  116. formattedValue = precision < 0
  117. ? f.ToString()
  118. : f.ToString($"G{precision}");
  119. break;
  120. case 'G':
  121. formattedValue = precision < 0
  122. ? f.ToString().ToUpper()
  123. : f.ToString($"G{precision}").ToUpper();
  124. break;
  125. }
  126. if (plusSign && f >= 0)
  127. {
  128. formattedValue = $"+{formattedValue}";
  129. }
  130. break;
  131. case 's':
  132. using (var strBuffer = new PooledArray<LuaValue>(1))
  133. {
  134. await parameter.CallToStringAsync(context, strBuffer.AsMemory(), cancellationToken);
  135. formattedValue = strBuffer[0].Read<string>();
  136. }
  137. if (specifier is 's' && precision > 0 && precision <= formattedValue.Length)
  138. {
  139. formattedValue = formattedValue[..precision];
  140. }
  141. break;
  142. case 'q':
  143. switch (parameter.Type)
  144. {
  145. case LuaValueType.Nil:
  146. formattedValue = "nil";
  147. break;
  148. case LuaValueType.Boolean:
  149. formattedValue = parameter.Read<bool>() ? "true" : "false";
  150. break;
  151. case LuaValueType.String:
  152. formattedValue = $"\"{StringHelper.Escape(parameter.Read<string>())}\"";
  153. break;
  154. case LuaValueType.Number:
  155. // TODO: floating point numbers must be in hexadecimal notation
  156. formattedValue = parameter.Read<double>().ToString();
  157. break;
  158. default:
  159. using (var strBuffer = new PooledArray<LuaValue>(1))
  160. {
  161. await parameter.CallToStringAsync(context, strBuffer.AsMemory(), cancellationToken);
  162. formattedValue = strBuffer[0].Read<string>();
  163. }
  164. break;
  165. }
  166. break;
  167. case 'i':
  168. case 'd':
  169. case 'u':
  170. case 'c':
  171. case 'x':
  172. case 'X':
  173. if (!parameter.TryRead<double>(out var x))
  174. {
  175. LuaRuntimeException.BadArgument(context.State.GetTraceback(), parameterIndex + 1, "format", LuaValueType.Number.ToString(), parameter.Type.ToString());
  176. }
  177. LuaRuntimeException.ThrowBadArgumentIfNumberIsNotInteger(context.State, this, parameterIndex + 1, x);
  178. switch (specifier)
  179. {
  180. case 'i':
  181. case 'd':
  182. {
  183. var integer = checked((long)x);
  184. formattedValue = precision < 0
  185. ? integer.ToString()
  186. : integer.ToString($"D{precision}");
  187. }
  188. break;
  189. case 'u':
  190. {
  191. var integer = checked((ulong)x);
  192. formattedValue = precision < 0
  193. ? integer.ToString()
  194. : integer.ToString($"D{precision}");
  195. }
  196. break;
  197. case 'c':
  198. formattedValue = ((char)(int)x).ToString();
  199. break;
  200. case 'x':
  201. {
  202. var integer = checked((ulong)x);
  203. formattedValue = alternateForm
  204. ? $"0x{integer:x}"
  205. : $"{integer:x}";
  206. }
  207. break;
  208. case 'X':
  209. {
  210. var integer = checked((ulong)x);
  211. formattedValue = alternateForm
  212. ? $"0X{integer:X}"
  213. : $"{integer:X}";
  214. }
  215. break;
  216. case 'o':
  217. {
  218. var integer = checked((long)x);
  219. formattedValue = Convert.ToString(integer, 8);
  220. }
  221. break;
  222. }
  223. if (plusSign && x >= 0)
  224. {
  225. formattedValue = $"+{formattedValue}";
  226. }
  227. break;
  228. default:
  229. throw new LuaRuntimeException(context.State.GetTraceback(), $"invalid option '%{specifier}' to 'format'");
  230. }
  231. // Apply blank (' ') flag for positive numbers
  232. if (specifier is 'd' or 'i' or 'f' or 'g' or 'G')
  233. {
  234. if (blank && !leftJustify && !zeroPadding && parameter.Read<double>() >= 0)
  235. {
  236. formattedValue = $" {formattedValue}";
  237. }
  238. }
  239. // Apply width and padding
  240. if (width > formattedValue.Length)
  241. {
  242. if (leftJustify)
  243. {
  244. formattedValue = formattedValue.PadRight(width);
  245. }
  246. else
  247. {
  248. formattedValue = zeroPadding ? formattedValue.PadLeft(width, '0') : formattedValue.PadLeft(width);
  249. }
  250. }
  251. builder.Append(formattedValue);
  252. }
  253. else
  254. {
  255. builder.Append(format[i]);
  256. }
  257. }
  258. buffer.Span[0] = builder.ToString();
  259. return 1;
  260. }
  261. }