ExamplesPlaygroundReference Source

coral-spectrum/coral-datetime/src/scripts/DateTime.js

  1. /**
  2. * Copyright 2019 Adobe. All rights reserved.
  3. * This file is licensed to you under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License. You may obtain a copy
  5. * of the License at http://www.apache.org/licenses/LICENSE-2.0
  6. *
  7. * Unless required by applicable law or agreed to in writing, software distributed under
  8. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
  9. * OF ANY KIND, either express or implied. See the License for the specific language
  10. * governing permissions and limitations under the License.
  11. */
  12.  
  13. // todo add tests
  14.  
  15. // Used to store DateTimeFormat
  16. const dateTimeFormats = {};
  17.  
  18. // Default supported format
  19. const DEFAULT_FORMAT = 'YYYY-MM-DD';
  20.  
  21. const transform2digit = (value) => {
  22. const s = value.toString();
  23. return s.length === 1 ? `0${s}` : s;
  24. };
  25.  
  26. // Default locale
  27. let globalLocale = document.documentElement.lang || window.navigator.language || 'en-US';
  28.  
  29. // Uses Intl.DateTimeFormat to return a formatted date string
  30. const formatDate = function (date, locale, options) {
  31. let formattedDateString = '';
  32. try {
  33. const key = `${JSON.stringify(locale)}${JSON.stringify(options)}`;
  34. const dateTimeFormat = dateTimeFormats[key];
  35.  
  36. // Use existing DateTimeFormat or create new one
  37. if (!dateTimeFormat) {
  38. dateTimeFormats[key] = new window.Intl.DateTimeFormat(locale, options);
  39. }
  40.  
  41. // Format to string
  42. formattedDateString = dateTimeFormats[key].format(date);
  43. } catch (e) {
  44. console.warn(e.message);
  45. }
  46.  
  47. return formattedDateString;
  48. };
  49.  
  50. /**
  51. The DateTime API is used as fallback to {@link momentJS}.
  52.  
  53. @param {DateTime|Date|Array<Number>|String} value
  54. The initial date value. If none provided, the current day is used instead.
  55. */
  56. class DateTime {
  57. /**
  58. @see https://momentjs.com/docs/#/parsing/now/
  59. */
  60. constructor(value) {
  61. if (value instanceof this.constructor) {
  62. // Copy properties
  63. this._locale = value._locale;
  64. this._value = value._value;
  65. this._date = value._date;
  66. } else {
  67. this._locale = globalLocale;
  68. this._value = value;
  69.  
  70. // Support Array
  71. if (Array.isArray(value)) {
  72. this._date = value.length ? new Date(value[0], value[1] || 0, value[2] || 1) : new Date();
  73. } else if (typeof value === 'string') {
  74. const isTime = value.indexOf(':') === 2;
  75.  
  76. // For time, we only need to set hours and minutes using current date
  77. if (isTime) {
  78. const time = value.split(':');
  79. const hours = parseInt(time[0], 10);
  80. const minutes = parseInt(time[1], 10);
  81.  
  82. if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) {
  83. this._date = new Date();
  84. this._date.setHours(time[0]);
  85. this._date.setMinutes(time[1]);
  86. } else {
  87. this._date = new Date('Invalid Date');
  88. }
  89. } else {
  90. // If string is invalid, the date will be invalid too
  91. // "replace" fixes the one day off issue
  92. this._date = new Date(this._value.replace(/-/g, '/').replace(/T.+/, ''));
  93. }
  94. } else if (this._value === null) {
  95. this._date = new Date('Invalid Date');
  96. } else {
  97. // Create a Date instance from the value or use current day if value is missing
  98. this._date = this._value ? new Date(this._value) : new Date();
  99. }
  100. }
  101. }
  102.  
  103. /**
  104. @see https://momentjs.com/docs/#/i18n/instance-locale/
  105. */
  106. locale(value) {
  107. if (value) {
  108. this._locale = value;
  109. }
  110.  
  111. return this._locale;
  112. }
  113.  
  114. /**
  115. @see https://momentjs.com/docs/#/displaying/as-javascript-date/
  116. */
  117. toDate() {
  118. return this._date;
  119. }
  120.  
  121. /**
  122. @see https://momentjs.com/docs/#/parsing/moment-clone/
  123. */
  124. clone() {
  125. const clone = new this.constructor(this._value);
  126. clone._date = this._date;
  127. return clone;
  128. }
  129.  
  130. /**
  131. @see https://momentjs.com/docs/#/displaying/format/
  132. */
  133. format(format) {
  134. let formattedDateString = '';
  135.  
  136. if (!format) {
  137. format = DEFAULT_FORMAT;
  138. }
  139.  
  140. if (format === DEFAULT_FORMAT) {
  141. formattedDateString += this._date.getFullYear();
  142. formattedDateString += '-';
  143. formattedDateString += transform2digit(this._date.getMonth() + 1);
  144. formattedDateString += '-';
  145. formattedDateString += transform2digit(this._date.getDate());
  146. } else if (format === 'MMMM YYYY') {
  147. formattedDateString += formatDate(this._date, this._locale, {month: 'long'});
  148. formattedDateString += ' ';
  149. formattedDateString += this._date.getFullYear();
  150. } else if (format === 'LL') {
  151. formattedDateString += formatDate(this._date, this._locale, {
  152. month: 'long',
  153. year: 'numeric',
  154. day: '2-digit'
  155. });
  156. } else if (format === 'dd') {
  157. formattedDateString += formatDate(this._date, this._locale, {weekday: 'short'});
  158. } else if (format === 'dddd') {
  159. formattedDateString += formatDate(this._date, this._locale, {weekday: 'long'});
  160. } else if (format === 'HH:mm') {
  161. formattedDateString += transform2digit(this._date.getHours());
  162. formattedDateString += ':';
  163. formattedDateString += transform2digit(this._date.getMinutes());
  164. } else if (format === 'HH') {
  165. formattedDateString += transform2digit(this._date.getHours());
  166. } else if (format === 'mm') {
  167. formattedDateString += transform2digit(this._date.getMinutes());
  168. } else if (format === 'YYYY-MM-DD[T]HH:mmZ') {
  169. formattedDateString += this._date.getFullYear();
  170. formattedDateString += '-';
  171. formattedDateString += transform2digit(this._date.getMonth() + 1);
  172. formattedDateString += '-';
  173. formattedDateString += transform2digit(this._date.getDate());
  174.  
  175. formattedDateString += 'T';
  176.  
  177. formattedDateString += transform2digit(this._date.getHours());
  178. formattedDateString += ':';
  179. formattedDateString += transform2digit(this._date.getMinutes());
  180.  
  181. const timezone = -1 * (this._date.getTimezoneOffset() / 60);
  182. let abs = Math.abs(timezone);
  183. abs = abs < 10 ? `0${abs}` : abs.toString();
  184.  
  185. formattedDateString += timezone < 0 ? `-${abs}:00` : `+${abs}:00`;
  186. } else {
  187. format = typeof format === 'object' ? format : {};
  188. formattedDateString = formatDate(this._date, this._locale, format);
  189. }
  190.  
  191. return formattedDateString;
  192. }
  193.  
  194. /**
  195. @see https://momentjs.com/docs/#/get-set/year/
  196. */
  197. year() {
  198. return this._date.getFullYear();
  199. }
  200.  
  201. /**
  202. @see https://momentjs.com/docs/#/get-set/month/
  203. */
  204. month() {
  205. return this._date.getMonth();
  206. }
  207.  
  208. /**
  209. @see https://momentjs.com/docs/#/get-set/week/
  210. */
  211. week() {
  212. // Source : https://stackoverflow.com/questions/6117814/get-week-of-year-in-javascript-like-in-php
  213. const date = new Date(Date.UTC(this._date.getFullYear(), this._date.getMonth(), this._date.getDate()));
  214. const dayNum = date.getUTCDay() || 7;
  215. date.setUTCDate(date.getUTCDate() + 4 - dayNum);
  216. const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
  217. return Math.ceil(((date - yearStart) / 86400000 + 1) / 7);
  218. }
  219.  
  220. /**
  221. @see https://momentjs.com/docs/#/get-set/day/
  222. */
  223. day(day) {
  224. if (typeof day === 'number') {
  225. this._date.setDate(this._date.getDate() - (this._date.getDay() || 7) + day);
  226.  
  227. return this;
  228. }
  229.  
  230. return this._date.getDay();
  231. }
  232.  
  233. /**
  234. @see https://momentjs.com/docs/#/get-set/hour/
  235. */
  236. hours(hours) {
  237. if (typeof hours === 'number') {
  238. this._date.setHours(hours);
  239.  
  240. return this;
  241. }
  242.  
  243. return this._date.getHours();
  244. }
  245.  
  246. /**
  247. @see https://momentjs.com/docs/#/get-set/minute/
  248. */
  249. minutes(minutes) {
  250. if (typeof minutes === 'number') {
  251. this._date.setMinutes(minutes);
  252.  
  253. return this;
  254. }
  255.  
  256. return this._date.getMinutes();
  257. }
  258.  
  259. /**
  260. @see https://momentjs.com/docs/#/get-set/date/
  261. */
  262. date() {
  263. return this._date.getDate();
  264. }
  265.  
  266. /**
  267. @see https://momentjs.com/docs/#/manipulating/add/
  268. */
  269. add(value, type) {
  270. let multiplier = 1;
  271. switch (type) {
  272. case 'year':
  273. case 'years':
  274. multiplier = 12;
  275. case 'month':
  276. case 'months':
  277. const dayOfMonth = this._date.getDate();
  278. this._date.setMonth(this._date.getMonth() + multiplier * value);
  279. if (this._date.getDate() != dayOfMonth) {
  280. this._date.setDate(0);
  281. }
  282. break;
  283. case 'week':
  284. case 'weeks':
  285. multiplier = 7;
  286. case 'day':
  287. case 'days':
  288. this._date.setDate(this._date.getDate() + multiplier * value);
  289. break;
  290. }
  291.  
  292. return this;
  293. }
  294.  
  295. /**
  296. @see https://momentjs.com/docs/#/manipulating/subtract/
  297. */
  298. subtract(value, type) {
  299. let multiplier = 1;
  300. switch (type) {
  301. case 'year':
  302. case 'years':
  303. multiplier = 12;
  304. case 'month':
  305. case 'months':
  306. const dayOfMonth = this._date.getDate();
  307. this._date.setMonth(this._date.getMonth() - multiplier * value);
  308. if (this._date.getDate() != dayOfMonth) {
  309. this._date.setDate(0);
  310. }
  311. break;
  312. case 'week':
  313. case 'weeks':
  314. multiplier = 7;
  315. case 'day':
  316. case 'days':
  317. this._date.setDate(this._date.getDate() - multiplier * value);
  318. break;
  319. }
  320.  
  321. return this;
  322. }
  323.  
  324.  
  325. /**
  326. @see https://momentjs.com/docs/#/displaying/days-in-month/
  327. */
  328. daysInMonth() {
  329. return new Date(this._date.getFullYear(), this._date.getMonth() + 1, 0).getDate();
  330. }
  331.  
  332. /**
  333. @see https://momentjs.com/docs/#/displaying/difference/
  334. */
  335. diff(obj) {
  336. let diff = this._date.getTime() - obj._date.getTime();
  337.  
  338. let timezoneDiff = this._date.getTimezoneOffset() - obj._date.getTimezoneOffset();
  339. if (timezoneDiff !== 0) {
  340. diff -= timezoneDiff * 60000;
  341. }
  342.  
  343. return diff / 86400000;
  344. }
  345.  
  346. /**
  347. @see https://momentjs.com/docs/#/manipulating/start-of/
  348. */
  349. startOf(value) {
  350. if (value === 'day') {
  351. // Today
  352. this._date = new Date(this._date.getFullYear(), this._date.getMonth(), this._date.getDate());
  353. } else if (value === 'month') {
  354. this._date = new Date(this._date.getFullYear(), this._date.getMonth(), 1);
  355. } else if (value === 'year') {
  356. this._date = new Date(new Date().getFullYear(), 0, 1);
  357. }
  358.  
  359. return this;
  360. }
  361.  
  362. /**
  363. @see https://momentjs.com/docs/#/query/is-before/
  364. */
  365. isBefore(coralDate, unit) {
  366. if (coralDate && coralDate._date) {
  367. return unit ? coralDate[unit]() > this[unit]() : coralDate._date > this._date;
  368. }
  369.  
  370. return false;
  371. }
  372.  
  373. /**
  374. @see https://momentjs.com/docs/#/query/is-after/
  375. */
  376. isAfter(coralDate, unit) {
  377. if (coralDate && coralDate._date) {
  378. return unit ? coralDate[unit]() < this[unit]() : coralDate._date < this._date;
  379. }
  380.  
  381. return false;
  382. }
  383.  
  384. /**
  385. @see https://momentjs.com/docs/#/query/is-same/
  386. */
  387. isSame(obj, type) {
  388. if (type === 'hour') {
  389. return obj && obj.clone()._date.getHours() === this.clone()._date.getHours();
  390. } else if (type === 'minute') {
  391. return obj && obj.clone()._date.getMinutes() === this.clone()._date.getMinutes();
  392. } else if (type === 'day') {
  393. return obj && obj.clone().startOf('day')._date.getTime() === this.clone().startOf('day')._date.getTime();
  394. }
  395.  
  396. return obj && obj.clone()._date.getTime() === this.clone()._date.getTime();
  397. }
  398.  
  399. /**
  400. @see https://momentjs.com/docs/#/parsing/is-valid/
  401. */
  402. isValid() {
  403. return this._date.toString() !== 'Invalid Date';
  404. }
  405.  
  406. /**
  407. @ignore
  408. Not supported so we return an empty object
  409. */
  410. static localeData() {
  411. return {};
  412. }
  413.  
  414. /**
  415. @see https://momentjs.com/docs/#/i18n/changing-locale/
  416. */
  417. static locale(value) {
  418. if (value) {
  419. globalLocale = value;
  420. }
  421.  
  422. return globalLocale;
  423. }
  424.  
  425. /**
  426. @see https://momentjs.com/docs/#/query/is-a-moment/
  427. */
  428. static isMoment(obj) {
  429. return obj instanceof this;
  430. }
  431.  
  432. /**
  433. @return {momentJS|DateTime}
  434. */
  435. static get Moment() {
  436. return window.moment || this;
  437. }
  438. }
  439.  
  440. export default DateTime;