src/impl/util.js
import { Duration } from '../duration';
import { DateTime } from '../datetime';
import { Zone } from '../zone';
import { LocalZone } from '../zones/localZone';
import { IANAZone } from '../zones/IANAZone';
import { FixedOffsetZone } from '../zones/fixedOffsetZone';
import { Settings } from '../settings';
import { InvalidArgumentError } from '../errors';
/**
* @private
*/
export class Util {
static friendlyDuration(duration) {
if (Util.isNumber(duration)) {
return Duration.fromMillis(duration);
} else if (duration instanceof Duration) {
return duration;
} else if (duration instanceof Object) {
return Duration.fromObject(duration);
} else {
throw new InvalidArgumentError('Unknown duration argument');
}
}
static friendlyDateTime(dateTimeish) {
if (dateTimeish instanceof DateTime) {
return dateTimeish;
} else if (dateTimeish.valueOf && Util.isNumber(dateTimeish.valueOf())) {
return DateTime.fromJSDate(dateTimeish);
} else if (dateTimeish instanceof Object) {
return DateTime.fromObject(dateTimeish);
} else {
throw new InvalidArgumentError('Unknown datetime argument');
}
}
static maybeArray(thing) {
return Array.isArray(thing) ? thing : [thing];
}
static isUndefined(o) {
return typeof o === 'undefined';
}
static isNumber(o) {
return typeof o === 'number';
}
static isString(o) {
return typeof o === 'string';
}
static numberBetween(thing, bottom, top) {
return Util.isNumber(thing) && thing >= bottom && thing <= top;
}
static pad(input, n = 2) {
return ('0'.repeat(n) + input).slice(-n);
}
static towardZero(input) {
return input < 0 ? Math.ceil(input) : Math.floor(input);
}
// DateTime -> JS date such that the date's UTC time is the datetimes's local time
static asIfUTC(dt) {
const ts = dt.ts - dt.offset;
return new Date(ts);
}
// http://stackoverflow.com/a/15030117
static flatten(arr) {
return arr.reduce(
(flat, toFlatten) =>
flat.concat(Array.isArray(toFlatten) ? Util.flatten(toFlatten) : toFlatten),
[]
);
}
static bestBy(arr, by, compare) {
return arr.reduce((best, next) => {
const pair = [by(next), next];
if (!best) {
return pair;
} else if (compare.apply(null, [best[0], pair[0]]) === best[0]) {
return best;
} else {
return pair;
}
}, null)[1];
}
static pick(obj, keys) {
return keys.reduce((a, k) => {
a[k] = obj[k];
return a;
}, {});
}
static isLeapYear(year) {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
}
static daysInYear(year) {
return Util.isLeapYear(year) ? 366 : 365;
}
static daysInMonth(year, month) {
if (month === 2) {
return Util.isLeapYear(year) ? 29 : 28;
} else {
return [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
}
}
static parseZoneInfo(ts, offsetFormat, locale, timeZone = null) {
const date = new Date(ts),
intl = {
hour12: false,
// avoid AM/PM
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
};
if (timeZone) {
intl.timeZone = timeZone;
}
const modified = Object.assign({ timeZoneName: offsetFormat }, intl);
if (Intl.DateTimeFormat.prototype.formatToParts) {
const parsed = new Intl.DateTimeFormat(locale, modified)
.formatToParts(date)
.find(m => m.type.toLowerCase() === 'timezonename');
return parsed ? parsed.value : null;
} else {
// this probably doesn't work for all locales
const without = new Intl.DateTimeFormat(locale, intl).format(date),
included = new Intl.DateTimeFormat(locale, modified).format(date),
diffed = included.substring(without.length),
trimmed = diffed.replace(/^[, ]+/, '');
return trimmed;
}
}
static normalizeZone(input) {
if (input === null) {
return LocalZone.instance;
} else if (input instanceof Zone) {
return input;
} else if (Util.isString(input)) {
const lowered = input.toLowerCase();
if (lowered === 'local') return LocalZone.instance;
else if (lowered === 'utc') return FixedOffsetZone.utcInstance;
else if (IANAZone.isValidSpecier(lowered)) return new IANAZone(input);
else return FixedOffsetZone.parseSpecifier(lowered) || Settings.defaultZone;
} else if (Util.isNumber(input)) {
return FixedOffsetZone.instance(input);
} else if (typeof input === 'object' && input.offset) {
// This is dumb, but the instanceof check above doesn't seem to really work
// so we're duck checking it
return input;
} else {
return Settings.defaultZone;
}
}
static normalizeObject(obj, normalizer, ignoreUnknown = false) {
const normalized = {};
for (const u in obj) {
if (obj.hasOwnProperty(u)) {
const v = obj[u];
if (v !== null && !Util.isUndefined(v) && !Number.isNaN(v)) {
const mapped = normalizer(u, ignoreUnknown);
if (mapped) {
normalized[mapped] = v;
}
}
}
}
return normalized;
}
static timeObject(obj) {
return Util.pick(obj, ['hour', 'minute', 'second', 'millisecond']);
}
static untrucateYear(year) {
return year > 60 ? 1900 + year : 2000 + year;
}
// signedOffset('-5', '30') -> -330
static signedOffset(offHourStr, offMinuteStr) {
const offHour = parseInt(offHourStr, 10) || 0,
offMin = parseInt(offMinuteStr, 10) || 0,
offMinSigned = offHour < 0 ? -offMin : offMin;
return offHour * 60 + offMinSigned;
}
}