DateConstructor.cs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. using System.Globalization;
  2. using Jint.Collections;
  3. using Jint.Native.Function;
  4. using Jint.Native.Object;
  5. using Jint.Runtime;
  6. using Jint.Runtime.Descriptors;
  7. using Jint.Runtime.Interop;
  8. namespace Jint.Native.Date;
  9. /// <summary>
  10. /// https://tc39.es/ecma262/#sec-date-constructor
  11. /// </summary>
  12. internal sealed class DateConstructor : Constructor
  13. {
  14. internal static readonly DateTime Epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
  15. private static readonly string[] DefaultFormats = {
  16. "yyyy-MM-dd",
  17. "yyyy-MM",
  18. "yyyy"
  19. };
  20. private static readonly string[] SecondaryFormats = {
  21. "yyyy-MM-ddTHH:mm:ss.FFF",
  22. "yyyy-MM-ddTHH:mm:ss",
  23. "yyyy-MM-ddTHH:mm",
  24. // Formats used in DatePrototype toString methods
  25. "ddd MMM dd yyyy HH:mm:ss 'GMT'K",
  26. "ddd MMM dd yyyy",
  27. "HH:mm:ss 'GMT'K",
  28. // standard formats
  29. "yyyy-M-dTH:m:s.FFFK",
  30. "yyyy/M/dTH:m:s.FFFK",
  31. "yyyy-M-dTH:m:sK",
  32. "yyyy/M/dTH:m:sK",
  33. "yyyy-M-dTH:mK",
  34. "yyyy/M/dTH:mK",
  35. "yyyy-M-d H:m:s.FFFK",
  36. "yyyy/M/d H:m:s.FFFK",
  37. "yyyy-M-d H:m:sK",
  38. "yyyy/M/d H:m:sK",
  39. "yyyy-M-d H:mK",
  40. "yyyy/M/d H:mK",
  41. "yyyy-M-dK",
  42. "yyyy/M/dK",
  43. "yyyy-MK",
  44. "yyyy/MK",
  45. "yyyyK",
  46. "THH:mm:ss.FFFK",
  47. "THH:mm:ssK",
  48. "THH:mmK",
  49. "THHK"
  50. };
  51. private static readonly JsString _functionName = new JsString("Date");
  52. internal DateConstructor(
  53. Engine engine,
  54. Realm realm,
  55. FunctionPrototype functionPrototype,
  56. ObjectPrototype objectPrototype)
  57. : base(engine, realm, _functionName)
  58. {
  59. _prototype = functionPrototype;
  60. PrototypeObject = new DatePrototype(engine, this, objectPrototype);
  61. _length = new PropertyDescriptor(7, PropertyFlag.Configurable);
  62. _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
  63. }
  64. internal DatePrototype PrototypeObject { get; }
  65. protected override void Initialize()
  66. {
  67. const PropertyFlag LengthFlags = PropertyFlag.Configurable;
  68. const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
  69. var properties = new PropertyDictionary(3, checkExistingKeys: false)
  70. {
  71. ["parse"] = new(new ClrFunctionInstance(Engine, "parse", Parse, 1, LengthFlags), PropertyFlags),
  72. ["UTC"] = new(new ClrFunctionInstance(Engine, "UTC", Utc, 7, LengthFlags), PropertyFlags),
  73. ["now"] = new(new ClrFunctionInstance(Engine, "now", Now, 0, LengthFlags), PropertyFlags)
  74. };
  75. SetProperties(properties);
  76. }
  77. /// <summary>
  78. /// https://tc39.es/ecma262/#sec-date.parse
  79. /// </summary>
  80. private JsValue Parse(JsValue thisObj, JsValue[] arguments)
  81. {
  82. var date = TypeConverter.ToString(arguments.At(0));
  83. var negative = date.StartsWith("-");
  84. if (negative)
  85. {
  86. date = date.Substring(1);
  87. }
  88. var startParen = date.IndexOf('(');
  89. if (startParen != -1)
  90. {
  91. // informative text
  92. date = date.Substring(0, startParen);
  93. }
  94. date = date.Trim();
  95. if (!DateTime.TryParseExact(date, DefaultFormats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result))
  96. {
  97. if (!DateTime.TryParseExact(date, SecondaryFormats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result))
  98. {
  99. if (!DateTime.TryParse(date, Engine.Options.Culture, DateTimeStyles.AdjustToUniversal, out result))
  100. {
  101. if (!DateTime.TryParse(date, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result))
  102. {
  103. // fall back to trying with MimeKit
  104. if (DateUtils.TryParse(date, out var mimeKitResult))
  105. {
  106. return FromDateTimeOffset(mimeKitResult);
  107. }
  108. // unrecognized dates should return NaN (15.9.4.2)
  109. return JsNumber.DoubleNaN;
  110. }
  111. }
  112. }
  113. }
  114. return FromDateTime(result, negative);
  115. }
  116. private static JsValue Utc(JsValue thisObj, JsValue[] arguments)
  117. {
  118. var y = TypeConverter.ToNumber(arguments.At(0));
  119. var m = TypeConverter.ToNumber(arguments.At(1, JsNumber.PositiveZero));
  120. var dt = TypeConverter.ToNumber(arguments.At(2, JsNumber.PositiveOne));
  121. var h = TypeConverter.ToNumber(arguments.At(3, JsNumber.PositiveZero));
  122. var min = TypeConverter.ToNumber(arguments.At(4, JsNumber.PositiveZero));
  123. var s = TypeConverter.ToNumber(arguments.At(5, JsNumber.PositiveZero));
  124. var milli = TypeConverter.ToNumber(arguments.At(6, JsNumber.PositiveZero));
  125. var yInteger = TypeConverter.ToInteger(y);
  126. if (!double.IsNaN(y) && 0 <= yInteger && yInteger <= 99)
  127. {
  128. y = yInteger + 1900;
  129. }
  130. var finalDate = DatePrototype.MakeDate(
  131. DatePrototype.MakeDay(y, m, dt),
  132. DatePrototype.MakeTime(h, min, s, milli));
  133. return finalDate.TimeClip();
  134. }
  135. private static JsValue Now(JsValue thisObj, JsValue[] arguments)
  136. {
  137. return System.Math.Floor((DateTime.UtcNow - Epoch).TotalMilliseconds);
  138. }
  139. protected internal override JsValue Call(JsValue thisObject, JsValue[] arguments)
  140. {
  141. return PrototypeObject.ToString(Construct(Arguments.Empty, thisObject), Arguments.Empty);
  142. }
  143. /// <summary>
  144. /// https://tc39.es/ecma262/#sec-date
  145. /// </summary>
  146. public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget)
  147. {
  148. // fast path is building default, new Date()
  149. if (arguments.Length == 0 || newTarget.IsUndefined())
  150. {
  151. return OrdinaryCreateFromConstructor(
  152. newTarget,
  153. static intrinsics => intrinsics.Date.PrototypeObject,
  154. static (engine, _, dateValue) => new JsDate(engine, dateValue),
  155. (DateTime.UtcNow - Epoch).TotalMilliseconds);
  156. }
  157. return ConstructUnlikely(arguments, newTarget);
  158. }
  159. private JsDate ConstructUnlikely(JsValue[] arguments, JsValue newTarget)
  160. {
  161. double dv;
  162. if (arguments.Length == 1)
  163. {
  164. if (arguments[0] is JsDate date)
  165. {
  166. return Construct(date.DateValue);
  167. }
  168. var v = TypeConverter.ToPrimitive(arguments[0]);
  169. if (v.IsString())
  170. {
  171. return Construct(((JsNumber) Parse(Undefined, Arguments.From(v)))._value);
  172. }
  173. dv = TypeConverter.ToNumber(v);
  174. }
  175. else
  176. {
  177. var y = TypeConverter.ToNumber(arguments.At(0));
  178. var m = TypeConverter.ToNumber(arguments.At(1));
  179. var dt = TypeConverter.ToNumber(arguments.At(2, JsNumber.PositiveOne));
  180. var h = TypeConverter.ToNumber(arguments.At(3, JsNumber.PositiveZero));
  181. var min = TypeConverter.ToNumber(arguments.At(4, JsNumber.PositiveZero));
  182. var s = TypeConverter.ToNumber(arguments.At(5, JsNumber.PositiveZero));
  183. var milli = TypeConverter.ToNumber(arguments.At(6, JsNumber.PositiveZero));
  184. var yInteger = TypeConverter.ToInteger(y);
  185. if (!double.IsNaN(y) && 0 <= yInteger && yInteger <= 99)
  186. {
  187. y += 1900;
  188. }
  189. var finalDate = DatePrototype.MakeDate(
  190. DatePrototype.MakeDay(y, m, dt),
  191. DatePrototype.MakeTime(h, min, s, milli));
  192. dv = PrototypeObject.Utc(finalDate);
  193. }
  194. return OrdinaryCreateFromConstructor(
  195. newTarget,
  196. static intrinsics => intrinsics.Date.PrototypeObject,
  197. static (engine, _, dateValue) => new JsDate(engine, dateValue), dv);
  198. }
  199. public JsDate Construct(DateTimeOffset value) => Construct(value.UtcDateTime);
  200. public JsDate Construct(DateTime value) => Construct(FromDateTime(value));
  201. public JsDate Construct(double time)
  202. {
  203. return OrdinaryCreateFromConstructor(
  204. Undefined,
  205. static intrinsics => intrinsics.Date.PrototypeObject,
  206. static (engine, _, dateValue) => new JsDate(engine, dateValue), time);
  207. }
  208. private static long FromDateTimeOffset(DateTimeOffset dt, bool negative = false)
  209. {
  210. var dateAsUtc = dt.ToUniversalTime();
  211. double result;
  212. if (negative)
  213. {
  214. var zero = (Epoch - DateTime.MinValue).TotalMilliseconds;
  215. result = zero - TimeSpan.FromTicks(dateAsUtc.Ticks).TotalMilliseconds;
  216. result *= -1;
  217. }
  218. else
  219. {
  220. result = (dateAsUtc - Epoch).TotalMilliseconds;
  221. }
  222. return (long) System.Math.Floor(result);
  223. }
  224. internal long FromDateTime(DateTime dt, bool negative = false)
  225. {
  226. var convertToUtcAfter = dt.Kind == DateTimeKind.Unspecified;
  227. var dateAsUtc = dt.Kind == DateTimeKind.Local
  228. ? dt.ToUniversalTime()
  229. : DateTime.SpecifyKind(dt, DateTimeKind.Utc);
  230. double result;
  231. if (negative)
  232. {
  233. var zero = (Epoch - DateTime.MinValue).TotalMilliseconds;
  234. result = zero - TimeSpan.FromTicks(dateAsUtc.Ticks).TotalMilliseconds;
  235. result *= -1;
  236. }
  237. else
  238. {
  239. result = (dateAsUtc - Epoch).TotalMilliseconds;
  240. }
  241. if (convertToUtcAfter)
  242. {
  243. result = PrototypeObject.Utc(result);
  244. }
  245. return (long) System.Math.Floor(result);
  246. }
  247. }