options.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. /**
  2. * Parent class for errors.
  3. *
  4. * @remarks
  5. * Errors with `type: "warning"` do not necessarily indicate that the parser
  6. * encountered an error. In addition to a human-friendly `message`, may also
  7. * includes the `token` at which the error was encountered.
  8. *
  9. * @public
  10. */
  11. export class DateFormatError extends Error {
  12. /** @internal */
  13. constructor(msg, token, type) {
  14. super(msg);
  15. this.token = token;
  16. this.type = type || 'error';
  17. }
  18. }
  19. const alpha = (width) => width < 4 ? 'short' : width === 4 ? 'long' : 'narrow';
  20. const numeric = (width) => (width % 2 === 0 ? '2-digit' : 'numeric');
  21. function yearOptions(token, onError) {
  22. switch (token.char) {
  23. case 'y':
  24. return { year: numeric(token.width) };
  25. case 'r':
  26. return { calendar: 'gregory', year: 'numeric' };
  27. case 'u':
  28. case 'U':
  29. case 'Y':
  30. default:
  31. onError(`${token.desc} is not supported; falling back to year:numeric`, DateFormatError.WARNING);
  32. return { year: 'numeric' };
  33. }
  34. }
  35. function monthStyle(token, onError) {
  36. switch (token.width) {
  37. case 1:
  38. return 'numeric';
  39. case 2:
  40. return '2-digit';
  41. case 3:
  42. return 'short';
  43. case 4:
  44. return 'long';
  45. case 5:
  46. return 'narrow';
  47. default:
  48. onError(`${token.desc} is not supported with width ${token.width}`);
  49. return undefined;
  50. }
  51. }
  52. function dayStyle(token, onError) {
  53. const { char, desc, width } = token;
  54. if (char === 'd')
  55. return numeric(width);
  56. else {
  57. onError(`${desc} is not supported`);
  58. return undefined;
  59. }
  60. }
  61. function weekdayStyle(token, onError) {
  62. const { char, desc, width } = token;
  63. if ((char === 'c' || char === 'e') && width < 3) {
  64. // ignoring stand-alone-ness
  65. const msg = `Numeric value is not supported for ${desc}; falling back to weekday:short`;
  66. onError(msg, DateFormatError.WARNING);
  67. }
  68. // merging narrow styles
  69. return alpha(width);
  70. }
  71. function hourOptions(token) {
  72. const hour = numeric(token.width);
  73. let hourCycle;
  74. switch (token.char) {
  75. case 'h':
  76. hourCycle = 'h12';
  77. break;
  78. case 'H':
  79. hourCycle = 'h23';
  80. break;
  81. case 'k':
  82. hourCycle = 'h24';
  83. break;
  84. case 'K':
  85. hourCycle = 'h11';
  86. break;
  87. }
  88. return hourCycle ? { hour, hourCycle } : { hour };
  89. }
  90. function timeZoneNameStyle(token, onError) {
  91. // so much fallback behaviour here
  92. const { char, desc, width } = token;
  93. switch (char) {
  94. case 'v':
  95. case 'z':
  96. return width === 4 ? 'long' : 'short';
  97. case 'V':
  98. if (width === 4)
  99. return 'long';
  100. onError(`${desc} is not supported with width ${width}`);
  101. return undefined;
  102. case 'X':
  103. onError(`${desc} is not supported`);
  104. return undefined;
  105. }
  106. return 'short';
  107. }
  108. function compileOptions(token, onError) {
  109. switch (token.field) {
  110. case 'era':
  111. return { era: alpha(token.width) };
  112. case 'year':
  113. return yearOptions(token, onError);
  114. case 'month':
  115. return { month: monthStyle(token, onError) };
  116. case 'day':
  117. return { day: dayStyle(token, onError) };
  118. case 'weekday':
  119. return { weekday: weekdayStyle(token, onError) };
  120. case 'period':
  121. return undefined;
  122. case 'hour':
  123. return hourOptions(token);
  124. case 'min':
  125. return { minute: numeric(token.width) };
  126. case 'sec':
  127. return { second: numeric(token.width) };
  128. case 'tz':
  129. return { timeZoneName: timeZoneNameStyle(token, onError) };
  130. case 'quarter':
  131. case 'week':
  132. case 'sec-frac':
  133. case 'ms':
  134. onError(`${token.desc} is not supported`);
  135. }
  136. return undefined;
  137. }
  138. export function getDateFormatOptions(tokens, onError = error => {
  139. throw error;
  140. }) {
  141. const options = {};
  142. const fields = [];
  143. for (const token of tokens) {
  144. const { error, field, str } = token;
  145. if (error) {
  146. const dte = new DateFormatError(error.message, token);
  147. dte.stack = error.stack;
  148. onError(dte);
  149. }
  150. if (str) {
  151. const msg = `Ignoring string part: ${str}`;
  152. onError(new DateFormatError(msg, token, DateFormatError.WARNING));
  153. }
  154. if (field) {
  155. if (fields.indexOf(field) === -1)
  156. fields.push(field);
  157. else
  158. onError(new DateFormatError(`Duplicate ${field} token`, token));
  159. }
  160. const opt = compileOptions(token, (msg, isWarning) => onError(new DateFormatError(msg, token, isWarning)));
  161. if (opt)
  162. Object.assign(options, opt);
  163. }
  164. return options;
  165. }