NumberPrototype.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. using System.Diagnostics;
  2. using System.Globalization;
  3. using System.Text;
  4. using Jint.Native.Number.Dtoa;
  5. using Jint.Native.Object;
  6. using Jint.Runtime;
  7. using Jint.Runtime.Descriptors;
  8. using Jint.Runtime.Interop;
  9. namespace Jint.Native.Number;
  10. /// <summary>
  11. /// https://tc39.es/ecma262/#sec-properties-of-the-number-prototype-object
  12. /// </summary>
  13. internal sealed class NumberPrototype : NumberInstance
  14. {
  15. private const int SmallDtoaLength = FastDtoa.KFastDtoaMaximalLength + 8;
  16. private const int LargeDtoaLength = 101;
  17. private readonly Realm _realm;
  18. private readonly NumberConstructor _constructor;
  19. internal NumberPrototype(
  20. Engine engine,
  21. Realm realm,
  22. NumberConstructor constructor,
  23. ObjectPrototype objectPrototype)
  24. : base(engine, InternalTypes.Object | InternalTypes.PlainObject)
  25. {
  26. _prototype = objectPrototype;
  27. _realm = realm;
  28. _constructor = constructor;
  29. }
  30. protected override void Initialize()
  31. {
  32. var properties = new PropertyDictionary(8, checkExistingKeys: false)
  33. {
  34. ["constructor"] = new PropertyDescriptor(_constructor, true, false, true),
  35. ["toString"] = new PropertyDescriptor(new ClrFunction(Engine, "toString", ToNumberString, 1, PropertyFlag.Configurable), true, false, true),
  36. ["toLocaleString"] = new PropertyDescriptor(new ClrFunction(Engine, "toLocaleString", ToLocaleString, 0, PropertyFlag.Configurable), true, false, true),
  37. ["valueOf"] = new PropertyDescriptor(new ClrFunction(Engine, "valueOf", ValueOf, 0, PropertyFlag.Configurable), true, false, true),
  38. ["toFixed"] = new PropertyDescriptor(new ClrFunction(Engine, "toFixed", ToFixed, 1, PropertyFlag.Configurable), true, false, true),
  39. ["toExponential"] = new PropertyDescriptor(new ClrFunction(Engine, "toExponential", ToExponential, 1, PropertyFlag.Configurable), true, false, true),
  40. ["toPrecision"] = new PropertyDescriptor(new ClrFunction(Engine, "toPrecision", ToPrecision, 1, PropertyFlag.Configurable), true, false, true)
  41. };
  42. SetProperties(properties);
  43. }
  44. /// <summary>
  45. /// https://tc39.es/ecma262/#sec-number.prototype.tolocalestring
  46. /// </summary>
  47. private JsValue ToLocaleString(JsValue thisObject, JsCallArguments arguments)
  48. {
  49. if (!thisObject.IsNumber() && thisObject is not NumberInstance)
  50. {
  51. Throw.TypeError(_realm);
  52. }
  53. var m = TypeConverter.ToNumber(thisObject);
  54. if (double.IsNaN(m))
  55. {
  56. return "NaN";
  57. }
  58. if (m == 0)
  59. {
  60. return JsString.NumberZeroString;
  61. }
  62. if (m < 0)
  63. {
  64. return "-" + ToLocaleString(-m, arguments);
  65. }
  66. if (double.IsPositiveInfinity(m) || m >= double.MaxValue)
  67. {
  68. return "Infinity";
  69. }
  70. if (double.IsNegativeInfinity(m) || m <= -double.MaxValue)
  71. {
  72. return "-Infinity";
  73. }
  74. var numberFormat = (NumberFormatInfo) Engine.Options.Culture.NumberFormat.Clone();
  75. try
  76. {
  77. if (arguments.Length > 0 && arguments[0].IsString())
  78. {
  79. var cultureArgument = arguments[0].ToString();
  80. numberFormat = (NumberFormatInfo) CultureInfo.GetCultureInfo(cultureArgument).NumberFormat.Clone();
  81. }
  82. int decDigitCount = NumberIntlHelper.GetDecimalDigitCount(m);
  83. numberFormat.NumberDecimalDigits = decDigitCount;
  84. }
  85. catch (CultureNotFoundException)
  86. {
  87. Throw.RangeError(_realm, "Incorrect locale information provided");
  88. }
  89. return m.ToString("n", numberFormat);
  90. }
  91. private JsValue ValueOf(JsValue thisObject, JsCallArguments arguments)
  92. {
  93. if (thisObject is NumberInstance ni)
  94. {
  95. return ni.NumberData;
  96. }
  97. if (thisObject is JsNumber)
  98. {
  99. return thisObject;
  100. }
  101. Throw.TypeError(_realm);
  102. return null;
  103. }
  104. private const double Ten21 = 1e21;
  105. private JsValue ToFixed(JsValue thisObject, JsCallArguments arguments)
  106. {
  107. var f = (int) TypeConverter.ToInteger(arguments.At(0, 0));
  108. if (f < 0 || f > 100)
  109. {
  110. Throw.RangeError(_realm, "fractionDigits argument must be between 0 and 100");
  111. }
  112. // limitation with .NET, max is 99
  113. if (f == 100)
  114. {
  115. Throw.RangeError(_realm, "100 fraction digits is not supported due to .NET format specifier limitation");
  116. }
  117. var x = TypeConverter.ToNumber(thisObject);
  118. if (double.IsNaN(x))
  119. {
  120. return "NaN";
  121. }
  122. if (x >= Ten21)
  123. {
  124. return ToNumberString(x);
  125. }
  126. // handle non-decimal with greater precision
  127. if (System.Math.Abs(x - (long) x) < JsNumber.DoubleIsIntegerTolerance)
  128. {
  129. return ((long) x).ToString("f" + f, CultureInfo.InvariantCulture);
  130. }
  131. return x.ToString("f" + f, CultureInfo.InvariantCulture);
  132. }
  133. /// <summary>
  134. /// https://www.ecma-international.org/ecma-262/6.0/#sec-number.prototype.toexponential
  135. /// </summary>
  136. private JsValue ToExponential(JsValue thisObject, JsCallArguments arguments)
  137. {
  138. if (!thisObject.IsNumber() && ReferenceEquals(thisObject.TryCast<NumberInstance>(), null))
  139. {
  140. Throw.TypeError(_realm);
  141. }
  142. var x = TypeConverter.ToNumber(thisObject);
  143. var fractionDigits = arguments.At(0);
  144. if (fractionDigits.IsUndefined())
  145. {
  146. fractionDigits = JsNumber.PositiveZero;
  147. }
  148. var f = (int) TypeConverter.ToInteger(fractionDigits);
  149. if (double.IsNaN(x))
  150. {
  151. return "NaN";
  152. }
  153. if (double.IsInfinity(x))
  154. {
  155. return thisObject.ToString();
  156. }
  157. if (f < 0 || f > 100)
  158. {
  159. Throw.RangeError(_realm, "fractionDigits argument must be between 0 and 100");
  160. }
  161. if (arguments.At(0).IsUndefined())
  162. {
  163. f = -1;
  164. }
  165. bool negative = false;
  166. if (x < 0)
  167. {
  168. x = -x;
  169. negative = true;
  170. }
  171. int decimalPoint;
  172. var dtoaBuilder = new DtoaBuilder(stackalloc char[f == -1 ? SmallDtoaLength : LargeDtoaLength]);
  173. if (f == -1)
  174. {
  175. DtoaNumberFormatter.DoubleToAscii(
  176. ref dtoaBuilder,
  177. x,
  178. DtoaMode.Shortest,
  179. requested_digits: 0,
  180. out _,
  181. out decimalPoint);
  182. f = dtoaBuilder.Length - 1;
  183. }
  184. else
  185. {
  186. DtoaNumberFormatter.DoubleToAscii(
  187. ref dtoaBuilder,
  188. x,
  189. DtoaMode.Precision,
  190. requested_digits: f + 1,
  191. out _,
  192. out decimalPoint);
  193. }
  194. Debug.Assert(dtoaBuilder.Length > 0);
  195. Debug.Assert(dtoaBuilder.Length <= f + 1);
  196. int exponent = decimalPoint - 1;
  197. var result = CreateExponentialRepresentation(ref dtoaBuilder, exponent, negative, f + 1);
  198. return result;
  199. }
  200. private JsValue ToPrecision(JsValue thisObject, JsCallArguments arguments)
  201. {
  202. if (!thisObject.IsNumber() && ReferenceEquals(thisObject.TryCast<NumberInstance>(), null))
  203. {
  204. Throw.TypeError(_realm);
  205. }
  206. var x = TypeConverter.ToNumber(thisObject);
  207. var precisionArgument = arguments.At(0);
  208. if (precisionArgument.IsUndefined())
  209. {
  210. return TypeConverter.ToString(x);
  211. }
  212. var p = (int) TypeConverter.ToInteger(precisionArgument);
  213. if (double.IsNaN(x))
  214. {
  215. return "NaN";
  216. }
  217. if (double.IsInfinity(x))
  218. {
  219. return thisObject.ToString();
  220. }
  221. if (p < 1 || p > 100)
  222. {
  223. Throw.RangeError(_realm, "precision must be between 1 and 100");
  224. }
  225. var dtoaBuilder = new DtoaBuilder(stackalloc char[LargeDtoaLength]);
  226. DtoaNumberFormatter.DoubleToAscii(
  227. ref dtoaBuilder,
  228. x,
  229. DtoaMode.Precision,
  230. p,
  231. out var negative,
  232. out var decimalPoint);
  233. int exponent = decimalPoint - 1;
  234. if (exponent < -6 || exponent >= p)
  235. {
  236. return CreateExponentialRepresentation(ref dtoaBuilder, exponent, negative, p);
  237. }
  238. var sb = new ValueStringBuilder(stackalloc char[128]);
  239. // Use fixed notation.
  240. if (negative)
  241. {
  242. sb.Append('-');
  243. }
  244. if (decimalPoint <= 0)
  245. {
  246. sb.Append("0.");
  247. sb.Append('0', -decimalPoint);
  248. sb.Append(dtoaBuilder._chars.Slice(0, dtoaBuilder.Length));
  249. sb.Append('0', p - dtoaBuilder.Length);
  250. }
  251. else
  252. {
  253. int m = System.Math.Min(dtoaBuilder.Length, decimalPoint);
  254. sb.Append(dtoaBuilder._chars.Slice(0, m));
  255. sb.Append('0', System.Math.Max(0, decimalPoint - dtoaBuilder.Length));
  256. if (decimalPoint < p)
  257. {
  258. sb.Append('.');
  259. var extra = negative ? 2 : 1;
  260. if (dtoaBuilder.Length > decimalPoint)
  261. {
  262. int len = dtoaBuilder.Length - decimalPoint;
  263. int n = System.Math.Min(len, p - (sb.Length - extra));
  264. sb.Append(dtoaBuilder._chars.Slice(decimalPoint, n));
  265. }
  266. sb.Append('0', System.Math.Max(0, extra + (p - sb.Length)));
  267. }
  268. }
  269. return sb.ToString();
  270. }
  271. private static string CreateExponentialRepresentation(
  272. ref DtoaBuilder buffer,
  273. int exponent,
  274. bool negative,
  275. int significantDigits)
  276. {
  277. bool negativeExponent = false;
  278. if (exponent < 0)
  279. {
  280. negativeExponent = true;
  281. exponent = -exponent;
  282. }
  283. var sb = new ValueStringBuilder(stackalloc char[128]);
  284. if (negative)
  285. {
  286. sb.Append('-');
  287. }
  288. sb.Append(buffer[0]);
  289. if (significantDigits != 1)
  290. {
  291. sb.Append('.');
  292. sb.Append(buffer.Slice(1, buffer.Length - 1));
  293. int length = buffer.Length;
  294. sb.Append('0', significantDigits - length);
  295. }
  296. sb.Append('e');
  297. sb.Append(negativeExponent ? '-' : '+');
  298. sb.Append(exponent);
  299. return sb.ToString();
  300. }
  301. private JsValue ToNumberString(JsValue thisObject, JsCallArguments arguments)
  302. {
  303. if (!thisObject.IsNumber() && (ReferenceEquals(thisObject.TryCast<NumberInstance>(), null)))
  304. {
  305. Throw.TypeError(_realm);
  306. }
  307. var radix = arguments.At(0).IsUndefined()
  308. ? 10
  309. : (int) TypeConverter.ToInteger(arguments.At(0));
  310. if (radix < 2 || radix > 36)
  311. {
  312. Throw.RangeError(_realm, "radix must be between 2 and 36");
  313. }
  314. var x = TypeConverter.ToNumber(thisObject);
  315. if (double.IsNaN(x))
  316. {
  317. return "NaN";
  318. }
  319. if (x == 0)
  320. {
  321. return JsString.NumberZeroString;
  322. }
  323. if (double.IsPositiveInfinity(x) || x >= double.MaxValue)
  324. {
  325. return "Infinity";
  326. }
  327. if (x < 0)
  328. {
  329. return "-" + ToNumberString(-x, arguments);
  330. }
  331. if (radix == 10)
  332. {
  333. return ToNumberString(x);
  334. }
  335. var integer = (long) x;
  336. var fraction = x - integer;
  337. string result = NumberPrototype.ToBase(integer, radix);
  338. if (fraction != 0)
  339. {
  340. result += "." + NumberPrototype.ToFractionBase(fraction, radix);
  341. }
  342. return result;
  343. }
  344. internal static string ToBase(long n, int radix)
  345. {
  346. const string Digits = "0123456789abcdefghijklmnopqrstuvwxyz";
  347. if (n == 0)
  348. {
  349. return "0";
  350. }
  351. var sb = new ValueStringBuilder(stackalloc char[64]);
  352. while (n > 0)
  353. {
  354. var digit = (int) (n % radix);
  355. n /= radix;
  356. sb.Append(Digits[digit]);
  357. }
  358. sb.Reverse();
  359. return sb.ToString();
  360. }
  361. internal static string ToFractionBase(double n, int radix)
  362. {
  363. // based on the repeated multiplication method
  364. // http://www.mathpath.org/concepts/Num/frac.htm
  365. const string Digits = "0123456789abcdefghijklmnopqrstuvwxyz";
  366. if (n == 0)
  367. {
  368. return "0";
  369. }
  370. var result = new ValueStringBuilder(stackalloc char[64]);
  371. while (n > 0 && result.Length < 50) // arbitrary limit
  372. {
  373. var c = n * radix;
  374. var d = (int) c;
  375. n = c - d;
  376. result.Append(Digits[d]);
  377. }
  378. return result.ToString();
  379. }
  380. internal static string ToNumberString(double m)
  381. {
  382. if (double.IsNaN(m))
  383. {
  384. return "NaN";
  385. }
  386. if (m == 0)
  387. {
  388. return "0";
  389. }
  390. if (double.IsInfinity(m))
  391. {
  392. return double.IsNegativeInfinity(m) ? "-Infinity" : "Infinity";
  393. }
  394. var builder = new DtoaBuilder(stackalloc char[SmallDtoaLength]);
  395. DtoaNumberFormatter.DoubleToAscii(
  396. ref builder,
  397. m,
  398. DtoaMode.Shortest,
  399. 0,
  400. out var negative,
  401. out var decimal_point);
  402. var stringBuilder = new ValueStringBuilder(stackalloc char[64]);
  403. if (negative)
  404. {
  405. stringBuilder.Append('-');
  406. }
  407. if (builder.Length <= decimal_point && decimal_point <= 21)
  408. {
  409. // ECMA-262 section 9.8.1 step 6.
  410. stringBuilder.Append(builder._chars.Slice(0, builder.Length));
  411. stringBuilder.Append('0', decimal_point - builder.Length);
  412. }
  413. else if (0 < decimal_point && decimal_point <= 21)
  414. {
  415. // ECMA-262 section 9.8.1 step 7.
  416. stringBuilder.Append(builder._chars.Slice(0, decimal_point));
  417. stringBuilder.Append('.');
  418. stringBuilder.Append(builder._chars.Slice(decimal_point, builder.Length - decimal_point));
  419. }
  420. else if (decimal_point <= 0 && decimal_point > -6)
  421. {
  422. // ECMA-262 section 9.8.1 step 8.
  423. stringBuilder.Append("0.");
  424. stringBuilder.Append('0', -decimal_point);
  425. stringBuilder.Append(builder._chars.Slice(0, builder.Length));
  426. }
  427. else
  428. {
  429. // ECMA-262 section 9.8.1 step 9 and 10 combined.
  430. stringBuilder.Append(builder._chars[0]);
  431. if (builder.Length != 1)
  432. {
  433. stringBuilder.Append('.');
  434. stringBuilder.Append(builder._chars.Slice(1, builder.Length - 1));
  435. }
  436. stringBuilder.Append('e');
  437. stringBuilder.Append((decimal_point >= 0) ? '+' : '-');
  438. int exponent = decimal_point - 1;
  439. if (exponent < 0)
  440. {
  441. exponent = -exponent;
  442. }
  443. stringBuilder.Append(exponent);
  444. }
  445. return stringBuilder.ToString();
  446. }
  447. }