diff --git a/src/datetime.js b/src/datetime.js index d4b5424cf..0258031bd 100644 --- a/src/datetime.js +++ b/src/datetime.js @@ -53,6 +53,12 @@ import Invalid from "./impl/invalid.js"; const INVALID = "Invalid DateTime"; const MAX_DATE = 8.64e15; +const Precision = { + hours: 1, + minutes: 2, + seconds: 3, + milliseconds: 4, +}; function unsupportedZone(zone) { return new Invalid("unsupported zone", `the zone "${zone.name}" is not supported`); @@ -238,25 +244,30 @@ function toISOTime( suppressSeconds, suppressMilliseconds, includeOffset, - extendedZone + extendedZone, + precision ) { + let desiredPrecision = Precision[normalizeUnit(precision)]; + if (desiredPrecision === undefined) throw new InvalidUnitError(precision); + let c = padStart(o.c.hour); - if (extended) { - c += ":"; - c += padStart(o.c.minute); - if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) { - c += ":"; - } - } else { + if (desiredPrecision >= Precision.minutes) { + if (extended) c += ":"; c += padStart(o.c.minute); - } - - if (o.c.millisecond !== 0 || o.c.second !== 0 || !suppressSeconds) { - c += padStart(o.c.second); - if (o.c.millisecond !== 0 || !suppressMilliseconds) { - c += "."; - c += padStart(o.c.millisecond, 3); + if ( + desiredPrecision >= Precision.seconds && + !(suppressSeconds && o.c.millisecond === 0 && o.c.second === 0) + ) { + if (extended) c += ":"; + c += padStart(o.c.second); + + if ( + desiredPrecision >= Precision.milliseconds && + !(suppressMilliseconds && o.c.millisecond === 0) + ) { + c += "." + padStart(o.c.millisecond, 3); + } } } @@ -1819,10 +1830,12 @@ export default class DateTime { * @param {boolean} [opts.includeOffset=true] - include the offset, such as 'Z' or '-04:00' * @param {boolean} [opts.extendedZone=false] - add the time zone format extension * @param {string} [opts.format='extended'] - choose between the basic and extended format + * @param {string} [opts.precision='milliseconds'] - specify desired time precision: 'hours', 'minutes', 'seconds' or 'milliseconds' * @example DateTime.utc(1983, 5, 25).toISO() //=> '1982-05-25T00:00:00.000Z' * @example DateTime.now().toISO() //=> '2017-04-22T20:47:05.335-04:00' * @example DateTime.now().toISO({ includeOffset: false }) //=> '2017-04-22T20:47:05.335' * @example DateTime.now().toISO({ format: 'basic' }) //=> '20170422T204705.335-0400' + * @example DateTime.now().toISO({ precision: 'minute' }) //=> '2017-04-22T20:47Z' * @return {string|null} */ toISO({ @@ -1831,6 +1844,7 @@ export default class DateTime { suppressMilliseconds = false, includeOffset = true, extendedZone = false, + precision = "milliseconds", } = {}) { if (!this.isValid) { return null; @@ -1840,7 +1854,15 @@ export default class DateTime { let c = toISODate(this, ext); c += "T"; - c += toISOTime(this, ext, suppressSeconds, suppressMilliseconds, includeOffset, extendedZone); + c += toISOTime( + this, + ext, + suppressSeconds, + suppressMilliseconds, + includeOffset, + extendedZone, + precision + ); return c; } @@ -1878,10 +1900,12 @@ export default class DateTime { * @param {boolean} [opts.extendedZone=true] - add the time zone format extension * @param {boolean} [opts.includePrefix=false] - include the `T` prefix * @param {string} [opts.format='extended'] - choose between the basic and extended format + * @param {string} [opts.precision='milliseconds'] - specify desired time precision: 'hours', 'minutes', 'seconds' or 'milliseconds' * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime() //=> '07:34:19.361Z' * @example DateTime.utc().set({ hour: 7, minute: 34, seconds: 0, milliseconds: 0 }).toISOTime({ suppressSeconds: true }) //=> '07:34Z' * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ format: 'basic' }) //=> '073419.361Z' * @example DateTime.utc().set({ hour: 7, minute: 34 }).toISOTime({ includePrefix: true }) //=> 'T07:34:19.361Z' + * @example DateTime.utc().set({ hour: 7, minute: 34, second: 56 }).toISOTime({ precision: 'minute' }) //=> '07:34Z' * @return {string} */ toISOTime({ @@ -1891,6 +1915,7 @@ export default class DateTime { includePrefix = false, extendedZone = false, format = "extended", + precision = "milliseconds", } = {}) { if (!this.isValid) { return null; @@ -1905,7 +1930,8 @@ export default class DateTime { suppressSeconds, suppressMilliseconds, includeOffset, - extendedZone + extendedZone, + precision ) ); } diff --git a/test/datetime/format.test.js b/test/datetime/format.test.js index d598a38f7..c824d18af 100644 --- a/test/datetime/format.test.js +++ b/test/datetime/format.test.js @@ -1,6 +1,7 @@ /* global test expect */ import { DateTime, Zone, FixedOffsetZone } from "../../src/luxon"; +import { InvalidUnitError } from "../../src/errors"; const dtMaker = () => DateTime.fromObject( @@ -261,6 +262,35 @@ test("DateTime#toISOTime() returns null for invalid DateTimes", () => { expect(invalid.toISOTime()).toBe(null); }); +test("DateTime#toISOTime({precision}) truncates time to desired precision", () => { + expect(dt.toISOTime({ precision: "millisecond" })).toBe("09:23:54.123Z"); + expect(dt.toISOTime({ precision: "second" })).toBe("09:23:54Z"); + expect(dt.toISOTime({ precision: "minute" })).toBe("09:23Z"); + expect(dt.toISOTime({ precision: "hours" })).toBe("09Z"); +}); + +test("DateTime#toISOTime({precision}) throws when the precision is invalid", () => { + expect(() => dt.toISOTime({ precision: "ms" })).toThrow(InvalidUnitError); + expect(() => dt.toISOTime({ precision: "xxx" })).toThrow(InvalidUnitError); + expect(() => dt.toISOTime({ precision: null })).toThrow(InvalidUnitError); + expect(() => dt.toISOTime({ precision: "days" })).toThrow(InvalidUnitError); + expect(() => dt.toISOTime({ precision: "months" })).toThrow(InvalidUnitError); + expect(() => dt.toISOTime({ precision: "years" })).toThrow(InvalidUnitError); +}); + +test("DateTime#toISOTime({precision, suppressSeconds}) precision takes precedence", () => { + const dt2 = dt.set({ second: 0, millisecond: 0 }); + expect(dt2.toISOTime({ precision: "minute", suppressSeconds: true })).toBe("09:23Z"); + expect(dt2.toISOTime({ precision: "second", suppressSeconds: true })).toBe("09:23Z"); + expect(dt2.toISOTime({ precision: "millisecond", suppressSeconds: true })).toBe("09:23Z"); +}); + +test("DateTime#toISOTime({precision, suppressMilliseconds}) supress", () => { + expect( + dt.set({ millisecond: 0 }).toISOTime({ precision: "millisecond", suppressMilliseconds: true }) + ).toBe("09:23:54Z"); +}); + //------ // #toRFC2822() //------