date-fns-extra.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /* eslint-disable max-len */
  2. /* eslint-disable eqeqeq */
  3. import {
  4. toDate,
  5. format as dateFnsFormat,
  6. utcToZonedTime as dateFnsUtcToZonedTime,
  7. zonedTimeToUtc as dateFnsZonedTimeToUtc,
  8. OptionsWithTZ
  9. } from 'date-fns-tz';
  10. import { parse as dateFnsParse } from 'date-fns';
  11. /**
  12. * Need to be IANA logo without daylight saving time
  13. */
  14. export const IANAOffsetMap = [
  15. [-11, ['Pacific/Midway']],
  16. [-10, ['Pacific/Honolulu']],
  17. [-9.5, ['Pacific/Marquesas']],
  18. [-9, ['Pacific/Gambier']],
  19. [-8, ['Pacific/Pitcairn']],
  20. [-7, ['America/Phoenix']],
  21. [-6, ['America/Tegucigalpa']],
  22. [-5, ['America/Bogota']],
  23. [-4, ['America/Puerto_Rico']],
  24. [-3.5, ['America/St_Johns']], // No alternative daylight saving time zone
  25. [-3, ['America/Montevideo']],
  26. [-2, ['Atlantic/South_Georgia']],
  27. [-1, ['Atlantic/Cape_Verde']],
  28. [0, ['Africa/Accra']],
  29. [1, ['Africa/Bangui']],
  30. [2, ['Africa/Cairo']],
  31. [3, ['Asia/Bahrain', 'Indian/Antananarivo']],
  32. [3.5, ['Asia/Tehran']], // No alternative daylight saving time zone
  33. [4, ['Asia/Dubai', 'Asia/Muscat']],
  34. [4.5, ['Asia/Kabul']],
  35. [5, ['Asia/Samarkand', 'Asia/Karachi']],
  36. [5.5, ['Asia/Kolkata']],
  37. [5.75, ['Asia/Kathmandu']],
  38. [6, ['Asia/Dhaka']],
  39. [6.5, ['Asia/Rangoon', 'Asia/Rangoon']],
  40. [7, ['Asia/Jakarta', 'Asia/Phnom_Penh', 'Asia/Bangkok']],
  41. [8, ['Asia/Shanghai', 'Asia/Singapore']],
  42. [8.75, ['Australia/Eucla']],
  43. [9, ['Asia/Tokyo', 'Asia/Seoul', 'Asia/Pyongyang']],
  44. [9.5, ['Australia/Darwin']],
  45. [10, ['Pacific/Guam']],
  46. [10.5, ['Australia/Adelaide']], // No alternative daylight saving time zone
  47. [11, ['Pacific/Guadalcanal']],
  48. [12, ['Pacific/Funafuti']],
  49. [13, ['Pacific/Enderbury']],
  50. [13.75, ['Pacific/Chatham']], // No alternative daylight saving time zone
  51. [14, ['Pacific/Kiritimati']],
  52. ];
  53. /**
  54. * Etc/GMT* no DST
  55. * @see https://data.iana.org/time-zones/tzdb/etcetera
  56. */
  57. const IANAEtcGMTOffsetMap = {
  58. '0': 'Etc/GMT',
  59. '1': 'Etc/GMT-1',
  60. '2': 'Etc/GMT-2',
  61. '3': 'Etc/GMT-3',
  62. '4': 'Etc/GMT-4',
  63. '5': 'Etc/GMT-5',
  64. '6': 'Etc/GMT-6',
  65. '7': 'Etc/GMT-7',
  66. '8': 'Etc/GMT-8',
  67. '9': 'Etc/GMT-9',
  68. '10': 'Etc/GMT-10',
  69. '11': 'Etc/GMT-11',
  70. '12': 'Etc/GMT-12',
  71. '13': 'Etc/GMT-13',
  72. '14': 'Etc/GMT-14',
  73. '-1': 'Etc/GMT+1',
  74. '-2': 'Etc/GMT+2',
  75. '-3': 'Etc/GMT+3',
  76. '-4': 'Etc/GMT+4',
  77. '-5': 'Etc/GMT+5',
  78. '-6': 'Etc/GMT+6',
  79. '-7': 'Etc/GMT+7',
  80. '-8': 'Etc/GMT+8',
  81. '-9': 'Etc/GMT+9',
  82. '-10': 'Etc/GMT+10',
  83. '-11': 'Etc/GMT+11',
  84. '-12': 'Etc/GMT+12',
  85. };
  86. const GMTStringReg = /([\-\+]{1})(\d{2})\:(\d{2})/;
  87. /**
  88. *
  89. * @param {string|number} tz
  90. * @returns {number|undefined}
  91. */
  92. export const toIANA = (tz: string | number) => {
  93. let matches = null;
  94. if (typeof tz === 'string') {
  95. matches = tz.match(GMTStringReg);
  96. if (!matches) {
  97. return tz;
  98. }
  99. const symbol = parseInt(matches[1] + 1, 10); // => -1 or 1
  100. const hourOffset = parseInt(matches[2], 10);
  101. const minuteOffset = parseInt(matches[3], 10);
  102. tz = symbol * (hourOffset + minuteOffset / 60);
  103. }
  104. if (typeof tz === 'number') {
  105. // if tz can be transformed to a Etc/GMT* and browser supports it
  106. if (tz in IANAEtcGMTOffsetMap) {
  107. const etcGMTtimeZone = IANAEtcGMTOffsetMap[tz];
  108. if (isValidTimezoneIANAString(etcGMTtimeZone)) {
  109. return etcGMTtimeZone;
  110. }
  111. }
  112. const found = IANAOffsetMap.find(item => item[0] === tz);
  113. return found && found[1][0];
  114. }
  115. };
  116. const validIANATimezoneCache = {};
  117. /**
  118. * @see https://github.com/marnusw/date-fns-tz/blob/a92e0ad017d101a0c50e39a63ef5d322b4d849f6/src/_lib/tzParseTimezone/index.js#L137
  119. */
  120. export function isValidTimezoneIANAString(timeZoneString: string) {
  121. if (validIANATimezoneCache[timeZoneString]) return true;
  122. try {
  123. new Intl.DateTimeFormat(undefined, { timeZone: timeZoneString });
  124. validIANATimezoneCache[timeZoneString] = true;
  125. return true;
  126. } catch (error) {
  127. return false;
  128. }
  129. }
  130. /**
  131. *
  132. * @param {string | number | Date} date
  133. * @param {string} formatToken
  134. * @param {object} [options]
  135. * @param {string} [options.timeZone]
  136. * @returns {Date}
  137. */
  138. /* istanbul ignore next */
  139. const parse = (date: string | number | Date, formatToken: string, options?: any) => {
  140. if (typeof date === 'string') {
  141. date = dateFnsParse(date, formatToken, new Date(), options);
  142. }
  143. if (options && options.timeZone != null && options.timeZone !== '') {
  144. const timeZone = toIANA(options.timeZone);
  145. options = { ...options, timeZone };
  146. }
  147. return toDate(date, options);
  148. };
  149. /* istanbul ignore next */
  150. const format = (date: number | Date, formatToken: string, options?: any) => {
  151. if (options && options.timeZone != null && options.timeZone !== '') {
  152. const timeZone = toIANA(options.timeZone);
  153. options = { ...options, timeZone };
  154. date = dateFnsUtcToZonedTime(date, timeZone, options);
  155. }
  156. return dateFnsFormat(date, formatToken, options);
  157. };
  158. /**
  159. * Returns a Date which will format as the local time of any time zone from a specific UTC time
  160. *
  161. * @example
  162. * ```javascript
  163. * import { utcToZonedTime } from 'date-fns-tz'
  164. * const { isoDate, timeZone } = fetchInitialValues() // 2014-06-25T10:00:00.000Z, America/New_York
  165. * const date = utcToZonedTime(isoDate, timeZone) // In June 10am UTC is 6am in New York (-04:00)
  166. * renderDatePicker(date) // 2014-06-25 06:00:00 (in the system time zone)
  167. * renderTimeZoneSelect(timeZone) // America/New_York
  168. * ```
  169. *
  170. * @see https://github.com/marnusw/date-fns-tz#utctozonedtime
  171. */
  172. const utcToZonedTime = (date: string | number | Date, timeZone: string | number, options?: OptionsWithTZ) => dateFnsUtcToZonedTime(date, toIANA(timeZone), options);
  173. /**
  174. * Given a date and any time zone, returns a Date with the equivalent UTC time
  175. *
  176. * @example
  177. * ```
  178. * import { zonedTimeToUtc } from 'date-fns-tz'
  179. * const date = getDatePickerValue() // e.g. 2014-06-25 10:00:00 (picked in any time zone)
  180. * const timeZone = getTimeZoneValue() // e.g. America/Los_Angeles
  181. * const utcDate = zonedTimeToUtc(date, timeZone) // In June 10am in Los Angeles is 5pm UTC
  182. * postToServer(utcDate.toISOString(), timeZone) // post 2014-06-25T17:00:00.000Z, America/Los_Angeles
  183. * ```
  184. *
  185. * @see https://github.com/marnusw/date-fns-tz#zonedtimetoutc
  186. */
  187. const zonedTimeToUtc = (date: string | number | Date, timeZone: string | number, options?: OptionsWithTZ) => dateFnsZonedTimeToUtc(date, toIANA(timeZone), options);
  188. /**
  189. * return current system hour offset based on utc:
  190. *
  191. * ```
  192. * 8 => "GMT+08:00"
  193. * -9.5 => "GMT-09:30"
  194. * -8 => "GMT-08:00"
  195. * ```
  196. */
  197. const getCurrentTimeZone = () => new Date().getTimezoneOffset() / 60;
  198. export { format, parse, utcToZonedTime, zonedTimeToUtc, getCurrentTimeZone };