Skip to content

Commit a25a5f5

Browse files
authored
Merge pull request #1032 from Patternslib/relative-date
dont include the time in relative formatting if the original date is a date-only and not a date-time object.
2 parents af4c414 + ef13602 commit a25a5f5

File tree

6 files changed

+636
-154
lines changed

6 files changed

+636
-154
lines changed

src/core/utils.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import $ from "jquery";
22
import dom from "./dom";
33

4+
const _MS_PER_DAY = 1000 * 60 * 60 * 24; // Milliseconds per day.
5+
46
$.fn.safeClone = function () {
57
var $clone = this.clone();
68
// IE BUG : Placeholder text becomes actual value after deep clone on textarea
@@ -431,6 +433,12 @@ function isElementInViewport(el, partial = false, offset = 0) {
431433
}
432434
}
433435

436+
/* parseTime - Parse a duration from a string and return the parsed time in milliseconds.
437+
*
438+
* @param {String} time - A duration/time string like ``1ms``, ``1s`` or ``1m``.
439+
*
440+
* @returns {Number} - A integer which represents the parsed time in milliseconds.
441+
*/
434442
function parseTime(time) {
435443
var m = /^(\d+(?:\.\d+)?)\s*(\w*)/.exec(time);
436444
if (!m) {
@@ -642,6 +650,33 @@ const is_iso_date_time = (value, optional_time = false) => {
642650
return re_date_time.test(value);
643651
};
644652

653+
/**
654+
* Return true, if the given value is a valid ISO 8601 date string and without a time component.
655+
*
656+
* @param {String} value - The date value to be checked.
657+
* @return {Boolean} - True, if the given value is a valid ISO 8601 date string without a time component. False if not.
658+
*/
659+
const is_iso_date = (value) => {
660+
const re_date_time = /^\d{4}-[01]\d-[0-3]\d$/;
661+
return re_date_time.test(value);
662+
};
663+
664+
/**
665+
* Return the number of days between two dates.
666+
* Based on: https://stackoverflow.com/a/15289883/1337474
667+
*
668+
* @param {Date} date_1 - First date to compare. We will substract date_2 from date_1.
669+
* @param {Date} date_2 - Second date to compare.
670+
* @return {Number} - The number of days between the two dates.
671+
*/
672+
const date_diff = (date_1, date_2) => {
673+
// Discard the time and time-zone information.
674+
const utc_1 = Date.UTC(date_1.getFullYear(), date_1.getMonth(), date_1.getDate());
675+
const utc_2 = Date.UTC(date_2.getFullYear(), date_2.getMonth(), date_2.getDate());
676+
677+
return Math.floor((utc_1 - utc_2) / _MS_PER_DAY);
678+
};
679+
645680
var utils = {
646681
// pattern pimping - own module?
647682
jqueryPlugin: jqueryPlugin,
@@ -673,6 +708,8 @@ var utils = {
673708
escape_html: escape_html,
674709
unescape_html: unescape_html,
675710
is_iso_date_time: is_iso_date_time,
711+
is_iso_date: is_iso_date,
712+
date_diff: date_diff,
676713
getCSSValue: dom.get_css_value, // BBB: moved to dom. TODO: Remove in upcoming version.
677714
};
678715

src/core/utils.test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,3 +748,51 @@ describe("is_iso_date_time ...", function () {
748748
expect(utils.is_iso_date_time("2022-05-04T21", true)).toBe(false);
749749
});
750750
});
751+
752+
describe("is_iso_date ...", function () {
753+
it("detects valid date objects", () => {
754+
// Return true on a valid ISO 8601 date.
755+
expect(utils.is_iso_date("2022-05-04")).toBe(true);
756+
757+
// A time component is not allowed.
758+
expect(utils.is_iso_date("2022-05-04T21:00")).toBe(false);
759+
760+
// We actually do not strictly check for a valid datetime, just if the
761+
// format is correct.
762+
expect(utils.is_iso_date("2222-19-39")).toBe(true);
763+
764+
// But some basic constraints are in place
765+
expect(utils.is_iso_date("2222-20-40")).toBe(false);
766+
767+
// And this is for sure no valid date/time
768+
expect(utils.is_iso_date("not2-ok-40")).toBe(false);
769+
770+
// This neigher
771+
expect(utils.is_iso_date("2022-05-04ok")).toBe(false);
772+
});
773+
});
774+
775+
describe("date_diff ...", function () {
776+
it("4 days ago...", () => {
777+
const date_1 = new Date();
778+
date_1.setDate(date_1.getDate() - 4);
779+
const date_2 = new Date();
780+
expect(utils.date_diff(date_1, date_2)).toBe(-4);
781+
});
782+
it("4 days up...", () => {
783+
const date_1 = new Date();
784+
date_1.setDate(date_1.getDate() + 4);
785+
const date_2 = new Date();
786+
expect(utils.date_diff(date_1, date_2)).toBe(4);
787+
});
788+
it("1 day ago over DST change...", () => {
789+
const date_1 = new Date("Sun Oct 29 2022 10:00:00 GMT+0200"); // Before DST change
790+
const date_2 = new Date("Sun Oct 30 2022 10:00:00 GMT+0100"); // After DST change
791+
expect(utils.date_diff(date_1, date_2)).toBe(-1);
792+
});
793+
it("1 day up over DST change...", () => {
794+
const date_1 = new Date("Sun Oct 30 2022 10:00:00 GMT+0100"); // After DST change
795+
const date_2 = new Date("Sun Oct 29 2022 10:00:00 GMT+0200"); // Before DST change
796+
expect(utils.date_diff(date_1, date_2)).toBe(1);
797+
});
798+
});

src/pat/display-time/display-time.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Base from "../../core/base";
22
import Parser from "../../core/parser";
33
import dom from "../../core/dom";
44
import logging from "../../core/logging";
5+
import utils from "../../core/utils";
56

67
// Lazy loading modules.
78
let Moment;
@@ -33,8 +34,10 @@ export default Base.extend({
3334
try {
3435
await import(`moment/locale/${lang}.js`);
3536
Moment.locale(lang);
37+
this.lang = lang;
3638
} catch {
3739
Moment.locale("en");
40+
this.lang = "en";
3841
}
3942
log.debug(`Moment.js language used: ${lang}.`);
4043
this.format();
@@ -46,7 +49,28 @@ export default Base.extend({
4649
if (datetime) {
4750
const date = Moment(datetime, this.options.format, this.options.strict);
4851
if (this.options.fromNow === true) {
49-
out = date.fromNow(this.options.noSuffix);
52+
if (utils.is_iso_date(datetime)) {
53+
// date-only case.
54+
const rtf = new Intl.RelativeTimeFormat(this.lang, {
55+
numeric: "auto",
56+
});
57+
const date_diff = utils.date_diff(date.toDate(), new Date());
58+
out = date.calendar(null, {
59+
// when the date is closer, specify custom values
60+
lastWeek: `[${rtf.format(date_diff, "day")}]`, // translates to "x days ago"
61+
lastDay: `[${rtf.format(-1, "day")}]`, // translates to "yesterday"
62+
sameDay: `[${rtf.format(0, "day")}]`, // translates to "today"
63+
nextDay: `[${rtf.format(1, "day")}]`, // translates to "tomorrow"
64+
nextWeek: "dddd",
65+
// when the date is further away, use from-now functionality
66+
sameElse: () => {
67+
return `[${date.fromNow(this.options.noSuffix)}]`;
68+
},
69+
});
70+
} else {
71+
// datetime case.
72+
out = date.fromNow(this.options.noSuffix);
73+
}
5074
} else {
5175
out = date.format(this.options.outputFormat || undefined);
5276
}

src/pat/display-time/display-time.test.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,149 @@ describe("pat-display-time tests", () => {
185185

186186
expect(el.textContent).toBe("");
187187
});
188+
189+
describe("3 - from-now (relative date) with date only dates", () => {
190+
it("3.1 - Today", async () => {
191+
const date = new Date();
192+
const iso_date = date.toISOString().split("T")[0];
193+
194+
document.body.innerHTML = `
195+
<time
196+
class="pat-display-time"
197+
datetime="${iso_date}"
198+
data-pat-display-time="from-now: true">
199+
</time>
200+
`;
201+
const el = document.querySelector(".pat-display-time");
202+
203+
Pattern.init(el);
204+
await utils.timeout(1); // wait a tick for async to settle.
205+
206+
expect(el.textContent).toBe("today");
207+
});
208+
209+
it("3.2 - Tomorrow", async () => {
210+
const date = new Date();
211+
date.setDate(date.getDate() + 1);
212+
const iso_date = date.toISOString().split("T")[0];
213+
214+
document.body.innerHTML = `
215+
<time
216+
class="pat-display-time"
217+
datetime="${iso_date}"
218+
data-pat-display-time="from-now: true">
219+
</time>
220+
`;
221+
const el = document.querySelector(".pat-display-time");
222+
223+
Pattern.init(el);
224+
await utils.timeout(1); // wait a tick for async to settle.
225+
226+
expect(el.textContent).toBe("tomorrow");
227+
});
228+
229+
it("3.3 - Yesterday", async () => {
230+
const date = new Date();
231+
date.setDate(date.getDate() - 1);
232+
const iso_date = date.toISOString().split("T")[0];
233+
234+
document.body.innerHTML = `
235+
<time
236+
class="pat-display-time"
237+
datetime="${iso_date}"
238+
data-pat-display-time="from-now: true">
239+
</time>
240+
`;
241+
const el = document.querySelector(".pat-display-time");
242+
243+
Pattern.init(el);
244+
await utils.timeout(1); // wait a tick for async to settle.
245+
246+
expect(el.textContent).toBe("yesterday");
247+
});
248+
249+
it("3.4 - Next week", async () => {
250+
const date = new Date(); // Use the system date.
251+
date.setDate(date.getDate() + 4);
252+
const iso_date = date.toISOString().split("T")[0];
253+
254+
document.body.innerHTML = `
255+
<time
256+
class="pat-display-time"
257+
datetime="${iso_date}"
258+
data-pat-display-time="from-now: true">
259+
</time>
260+
`;
261+
const el = document.querySelector(".pat-display-time");
262+
263+
Pattern.init(el);
264+
await utils.timeout(1); // wait a tick for async to settle.
265+
266+
// Match any of these as we did not mock the system date.
267+
expect(el.textContent).toMatch(
268+
/Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday/
269+
);
270+
});
271+
272+
it("3.5 - Last week", async () => {
273+
const date = new Date(); // Use the system date.
274+
date.setDate(date.getDate() - 4);
275+
const iso_date = date.toISOString().split("T")[0];
276+
277+
document.body.innerHTML = `
278+
<time
279+
class="pat-display-time"
280+
datetime="${iso_date}"
281+
data-pat-display-time="from-now: true">
282+
</time>
283+
`;
284+
const el = document.querySelector(".pat-display-time");
285+
286+
Pattern.init(el);
287+
await utils.timeout(1); // wait a tick for async to settle.
288+
289+
// Match any of these as we did not mock the system date.
290+
expect(el.textContent).toBe("4 days ago");
291+
});
292+
293+
it("3.6 - 10 days in the future", async () => {
294+
const date = new Date();
295+
date.setDate(date.getDate() + 10);
296+
const iso_date = date.toISOString().split("T")[0];
297+
298+
document.body.innerHTML = `
299+
<time
300+
class="pat-display-time"
301+
datetime="${iso_date}"
302+
data-pat-display-time="from-now: true">
303+
</time>
304+
`;
305+
const el = document.querySelector(".pat-display-time");
306+
307+
Pattern.init(el);
308+
await utils.timeout(1); // wait a tick for async to settle.
309+
310+
expect(el.textContent).toBe("in 10 days");
311+
});
312+
313+
it("3.7 - 10 days in the past", async () => {
314+
const date = new Date();
315+
date.setDate(date.getDate() - 10);
316+
const iso_date = date.toISOString().split("T")[0];
317+
318+
document.body.innerHTML = `
319+
<time
320+
class="pat-display-time"
321+
datetime="${iso_date}"
322+
data-pat-display-time="from-now: true">
323+
</time>
324+
`;
325+
const el = document.querySelector(".pat-display-time");
326+
327+
Pattern.init(el);
328+
await utils.timeout(1); // wait a tick for async to settle.
329+
330+
expect(el.textContent).toBe("10 days ago");
331+
});
332+
});
188333
});

src/pat/display-time/documentation.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,23 @@ A pattern that makes dates easier to read.
44

55
## Documentation
66

7+
Example:
8+
9+
<time class="pat-display-time"
10+
datetime="2015-01-20T08:00Z"
11+
data-pat-display-time="from-now: true"
12+
>
13+
</time>
14+
15+
16+
### Options reference
17+
18+
| Property | Default value | Description | Type |
19+
| ----------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
20+
| `output-format` | | For non-relative dates (the default, `from-now` set to `false`) show the date in the given format. For formatting options see: https://momentjs.com/docs/#/displaying/format/ | String |
21+
| `from-now` | `false` | Change the date to a relative date, relative from now. | Boolean |
22+
| `no-suffix` | `false` | For relative dates and no-suffix is set to `true`, do not show the suffix like `8 years` instead of `8 years ago`. | Boolean |
23+
| `format` | | Input parsing format. If not given (the default) the format is set automatically, if possible. For more information, see: https://momentjs.com/docs/#/parsing/string-format/ | String |
24+
| `locale` | | The locale to translate the resulting date/time string into. If not given (the default) the locale is retrieved from a `lang` attribute up in the DOM tree or `en`. | String |
25+
| `strict` | `false` | Strict parsing for the input format. See: https://momentjs.com/guides/#/parsing/strict-mode/ | Boolean |
26+

0 commit comments

Comments
 (0)