diff --git a/src/datetime.js b/src/datetime.js index 160b7bbd7..ef1ea3d38 100644 --- a/src/datetime.js +++ b/src/datetime.js @@ -447,8 +447,9 @@ function quickDT(obj, opts) { function diffRelative(start, end, opts) { const round = isUndefined(opts.round) ? true : opts.round, + rounding = isUndefined(opts.rounding) ? "trunc" : opts.rounding, format = (c, unit) => { - c = roundTo(c, round || opts.calendary ? 0 : 2, true); + c = roundTo(c, round || opts.calendary ? 0 : 2, opts.calendary ? "round" : rounding); const formatter = end.loc.clone(opts).relFormatter(opts); return formatter.format(c, unit); }, @@ -2193,12 +2194,13 @@ export default class DateTime { /** * Returns a string representation of a this time relative to now, such as "in two days". Can only internationalize if your - * platform supports Intl.RelativeTimeFormat. Rounds down by default. + * platform supports Intl.RelativeTimeFormat. Rounds towards zero by default. * @param {Object} options - options that affect the output * @param {DateTime} [options.base=DateTime.now()] - the DateTime to use as the basis to which this time is compared. Defaults to now. * @param {string} [options.style="long"] - the style of units, must be "long", "short", or "narrow" * @param {string|string[]} options.unit - use a specific unit or array of units; if omitted, or an array, the method will pick the best unit. Use an array or one of "years", "quarters", "months", "weeks", "days", "hours", "minutes", or "seconds" * @param {boolean} [options.round=true] - whether to round the numbers in the output. + * @param {string} [options.rounding="trunc"] - rounding method to use when rounding the numbers in the output. Can be "trunc" (toward zero), "expand" (away from zero), "round", "floor", or "ceil". * @param {number} [options.padding=0] - padding in milliseconds. This allows you to round up the result if it fits inside the threshold. Don't use in combination with {round: false} because the decimal output will include the padding. * @param {string} options.locale - override the locale of this DateTime * @param {string} options.numberingSystem - override the numberingSystem of this DateTime. The Intl system may choose not to honor this diff --git a/src/impl/util.js b/src/impl/util.js index a9b548c3c..8f8c47b58 100644 --- a/src/impl/util.js +++ b/src/impl/util.js @@ -159,10 +159,24 @@ export function parseMillis(fraction) { } } -export function roundTo(number, digits, towardZero = false) { - const factor = 10 ** digits, - rounder = towardZero ? Math.trunc : Math.round; - return rounder(number * factor) / factor; +export function roundTo(number, digits, rounding = "round") { + const factor = 10 ** digits; + switch (rounding) { + case "expand": + return number > 0 + ? Math.ceil(number * factor) / factor + : Math.floor(number * factor) / factor; + case "trunc": + return Math.trunc(number * factor) / factor; + case "round": + return Math.round(number * factor) / factor; + case "floor": + return Math.floor(number * factor) / factor; + case "ceil": + return Math.ceil(number * factor) / factor; + default: + throw new RangeError(`Value rounding ${rounding} is out of range`); + } } // DATE BASICS diff --git a/test/datetime/relative.test.js b/test/datetime/relative.test.js index 07453ea66..3caf060db 100644 --- a/test/datetime/relative.test.js +++ b/test/datetime/relative.test.js @@ -43,6 +43,144 @@ test("DateTime#toRelative takes a round argument", () => { expect(base.minus({ months: 15 }).toRelative({ base, round: false })).toBe("1.25 years ago"); }); +test("DateTime#toRelative takes a rounding argument", () => { + const base = DateTime.fromObject({ year: 1983, month: 10, day: 14 }); + expect(base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "expand" })).toBe( + "in 2 hours" + ); + expect(base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "expand" })).toBe( + "in 3 hours" + ); + expect(base.minus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "expand" })).toBe( + "2 hours ago" + ); + expect(base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "expand" })).toBe( + "3 hours ago" + ); + + expect(base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "trunc" })).toBe( + "in 1 hour" + ); + expect(base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "trunc" })).toBe( + "in 2 hours" + ); + expect(base.minus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "trunc" })).toBe( + "1 hour ago" + ); + expect(base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "trunc" })).toBe( + "2 hours ago" + ); + + expect(base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "round" })).toBe( + "in 2 hours" + ); + expect(base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "round" })).toBe( + "in 2 hours" + ); + expect(base.minus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "round" })).toBe( + "2 hours ago" + ); + expect(base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "round" })).toBe( + "2 hours ago" + ); + + expect(base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "floor" })).toBe( + "in 1 hour" + ); + expect(base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "floor" })).toBe( + "in 2 hours" + ); + expect(base.minus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "floor" })).toBe( + "2 hours ago" + ); + expect(base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "floor" })).toBe( + "3 hours ago" + ); + + expect(base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "ceil" })).toBe( + "in 2 hours" + ); + expect(base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "ceil" })).toBe( + "in 3 hours" + ); + expect(base.minus({ hours: 2, milliseconds: -1 }).toRelative({ base, rounding: "ceil" })).toBe( + "1 hour ago" + ); + expect(base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, rounding: "ceil" })).toBe( + "2 hours ago" + ); +}); + +test("DateTime#toRelative takes a round and a rounding argument", () => { + const base = DateTime.fromObject({ year: 1983, month: 10, day: 14 }); + expect( + base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, round: false, rounding: "expand" }) + ).toBe("in 2 hours"); + expect( + base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "expand" }) + ).toBe("in 2.01 hours"); + expect( + base + .minus({ hours: 2, milliseconds: -1 }) + .toRelative({ base, round: false, rounding: "expand" }) + ).toBe("2 hours ago"); + expect( + base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "expand" }) + ).toBe("2.01 hours ago"); + + expect( + base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, round: false, rounding: "trunc" }) + ).toBe("in 1.99 hours"); + expect( + base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "trunc" }) + ).toBe("in 2 hours"); + expect( + base.minus({ hours: 2, milliseconds: -1 }).toRelative({ base, round: false, rounding: "trunc" }) + ).toBe("1.99 hours ago"); + expect( + base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "trunc" }) + ).toBe("2 hours ago"); + + expect( + base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, round: false, rounding: "round" }) + ).toBe("in 2 hours"); + expect( + base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "round" }) + ).toBe("in 2 hours"); + expect( + base.minus({ hours: 2, milliseconds: -1 }).toRelative({ base, round: false, rounding: "round" }) + ).toBe("2 hours ago"); + expect( + base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "round" }) + ).toBe("2 hours ago"); + + expect( + base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, round: false, rounding: "floor" }) + ).toBe("in 1.99 hours"); + expect( + base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "floor" }) + ).toBe("in 2 hours"); + expect( + base.minus({ hours: 2, milliseconds: -1 }).toRelative({ base, round: false, rounding: "floor" }) + ).toBe("2 hours ago"); + expect( + base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "floor" }) + ).toBe("2.01 hours ago"); + + expect( + base.plus({ hours: 2, milliseconds: -1 }).toRelative({ base, round: false, rounding: "ceil" }) + ).toBe("in 2 hours"); + expect( + base.plus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "ceil" }) + ).toBe("in 2.01 hours"); + expect( + base.minus({ hours: 2, milliseconds: -1 }).toRelative({ base, round: false, rounding: "ceil" }) + ).toBe("1.99 hours ago"); + expect( + base.minus({ hours: 2, milliseconds: 1 }).toRelative({ base, round: false, rounding: "ceil" }) + ).toBe("2 hours ago"); +}); + test("DateTime#toRelative takes a unit argument", () => { const base = DateTime.fromObject({ year: 2018, month: 10, day: 14 }, { zone: "UTC" }); expect(base.plus({ months: 15 }).toRelative({ base, unit: "months" })).toBe("in 15 months");