date-format.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import {stripTime, today} from './date.js';
  2. import {lastItemOf} from './utils.js';
  3. // pattern for format parts
  4. export const reFormatTokens = /dd?|DD?|mm?|MM?|yy?(?:yy)?/;
  5. // pattern for non date parts
  6. export const reNonDateParts = /[\s!-/:-@[-`{-~年月日]+/;
  7. // cache for persed formats
  8. let knownFormats = {};
  9. // parse funtions for date parts
  10. const parseFns = {
  11. y(date, year) {
  12. return new Date(date).setFullYear(parseInt(year, 10));
  13. },
  14. m(date, month, locale) {
  15. const newDate = new Date(date);
  16. let monthIndex = parseInt(month, 10) - 1;
  17. if (isNaN(monthIndex)) {
  18. if (!month) {
  19. return NaN;
  20. }
  21. const monthName = month.toLowerCase();
  22. const compareNames = name => name.toLowerCase().startsWith(monthName);
  23. // compare with both short and full names because some locales have periods
  24. // in the short names (not equal to the first X letters of the full names)
  25. monthIndex = locale.monthsShort.findIndex(compareNames);
  26. if (monthIndex < 0) {
  27. monthIndex = locale.months.findIndex(compareNames);
  28. }
  29. if (monthIndex < 0) {
  30. return NaN;
  31. }
  32. }
  33. newDate.setMonth(monthIndex);
  34. return newDate.getMonth() !== normalizeMonth(monthIndex)
  35. ? newDate.setDate(0)
  36. : newDate.getTime();
  37. },
  38. d(date, day) {
  39. return new Date(date).setDate(parseInt(day, 10));
  40. },
  41. };
  42. // format functions for date parts
  43. const formatFns = {
  44. d(date) {
  45. return date.getDate();
  46. },
  47. dd(date) {
  48. return padZero(date.getDate(), 2);
  49. },
  50. D(date, locale) {
  51. return locale.daysShort[date.getDay()];
  52. },
  53. DD(date, locale) {
  54. return locale.days[date.getDay()];
  55. },
  56. m(date) {
  57. return date.getMonth() + 1;
  58. },
  59. mm(date) {
  60. return padZero(date.getMonth() + 1, 2);
  61. },
  62. M(date, locale) {
  63. return locale.monthsShort[date.getMonth()];
  64. },
  65. MM(date, locale) {
  66. return locale.months[date.getMonth()];
  67. },
  68. y(date) {
  69. return date.getFullYear();
  70. },
  71. yy(date) {
  72. return padZero(date.getFullYear(), 2).slice(-2);
  73. },
  74. yyyy(date) {
  75. return padZero(date.getFullYear(), 4);
  76. },
  77. };
  78. // get month index in normal range (0 - 11) from any number
  79. function normalizeMonth(monthIndex) {
  80. return monthIndex > -1 ? monthIndex % 12 : normalizeMonth(monthIndex + 12);
  81. }
  82. function padZero(num, length) {
  83. return num.toString().padStart(length, '0');
  84. }
  85. function parseFormatString(format) {
  86. if (typeof format !== 'string') {
  87. throw new Error("Invalid date format.");
  88. }
  89. if (format in knownFormats) {
  90. return knownFormats[format];
  91. }
  92. // sprit the format string into parts and seprators
  93. const separators = format.split(reFormatTokens);
  94. const parts = format.match(new RegExp(reFormatTokens, 'g'));
  95. if (separators.length === 0 || !parts) {
  96. throw new Error("Invalid date format.");
  97. }
  98. // collect format functions used in the format
  99. const partFormatters = parts.map(token => formatFns[token]);
  100. // collect parse function keys used in the format
  101. // iterate over parseFns' keys in order to keep the order of the keys.
  102. const partParserKeys = Object.keys(parseFns).reduce((keys, key) => {
  103. const token = parts.find(part => part[0] !== 'D' && part[0].toLowerCase() === key);
  104. if (token) {
  105. keys.push(key);
  106. }
  107. return keys;
  108. }, []);
  109. return knownFormats[format] = {
  110. parser(dateStr, locale) {
  111. const dateParts = dateStr.split(reNonDateParts).reduce((dtParts, part, index) => {
  112. if (part.length > 0 && parts[index]) {
  113. const token = parts[index][0];
  114. if (token === 'M') {
  115. dtParts.m = part;
  116. } else if (token !== 'D') {
  117. dtParts[token] = part;
  118. }
  119. }
  120. return dtParts;
  121. }, {});
  122. // iterate over partParserkeys so that the parsing is made in the oder
  123. // of year, month and day to prevent the day parser from correcting last
  124. // day of month wrongly
  125. return partParserKeys.reduce((origDate, key) => {
  126. const newDate = parseFns[key](origDate, dateParts[key], locale);
  127. // ingnore the part failed to parse
  128. return isNaN(newDate) ? origDate : newDate;
  129. }, today());
  130. },
  131. formatter(date, locale) {
  132. let dateStr = partFormatters.reduce((str, fn, index) => {
  133. return str += `${separators[index]}${fn(date, locale)}`;
  134. }, '');
  135. // separators' length is always parts' length + 1,
  136. return dateStr += lastItemOf(separators);
  137. },
  138. };
  139. }
  140. export function parseDate(dateStr, format, locale) {
  141. if (dateStr instanceof Date || typeof dateStr === 'number') {
  142. const date = stripTime(dateStr);
  143. return isNaN(date) ? undefined : date;
  144. }
  145. if (!dateStr) {
  146. return undefined;
  147. }
  148. if (dateStr === 'today') {
  149. return today();
  150. }
  151. if (format && format.toValue) {
  152. const date = format.toValue(dateStr, format, locale);
  153. return isNaN(date) ? undefined : stripTime(date);
  154. }
  155. return parseFormatString(format).parser(dateStr, locale);
  156. }
  157. export function formatDate(date, format, locale) {
  158. if (isNaN(date) || (!date && date !== 0)) {
  159. return '';
  160. }
  161. const dateObj = typeof date === 'number' ? new Date(date) : date;
  162. if (format.toDisplay) {
  163. return format.toDisplay(dateObj, format, locale);
  164. }
  165. return parseFormatString(format).formatter(dateObj, locale);
  166. }