UNPKG

stem-core

Version:

Frontend and core-library framework

428 lines (354 loc) 13.5 kB
import {padNumber, suffixWithOrdinal, extendsNative, instantiateNative, isNumber} from "../base/Utils"; import {TimeUnit, Duration} from "./Duration"; // MAX_UNIX_TIME is either ~Feb 2106 in unix seconds or ~Feb 1970 in unix milliseconds // Any value less than this is interpreted as a unix time in seconds // If you want to go around this behavious, you can use the static method .fromUnixMilliseconds() // To disable, set this value to 0 export let MAX_AUTO_UNIX_TIME = Math.pow(2, 32); const BaseDate = self.Date; @extendsNative class StemDate extends BaseDate { // This is only to let the IDE know that this class can receive arguments. constructor(...args) { super(...args); } // Still need to do this mess because of Babel, should be removed when moving to native ES6 static create(value) { // Try to do an educated guess if this date is in unix seconds or milliseconds if (arguments.length === 1 && isNumber(value) && value < MAX_AUTO_UNIX_TIME) { return instantiateNative(BaseDate, StemDate, value * 1000.0); } else { return instantiateNative(BaseDate, StemDate, ...arguments); } } static toDate(date) { if (date instanceof StemDate) { return date; } else { return new this(date); } } static now() { return new this(BaseDate.now()); } toDate() { return this; } static fromUnixMilliseconds(unixMilliseconds) { return this.create(new BaseDate(unixMilliseconds)); } static fromUnixSeconds(unixSecons) { return this.fromUnixMilliseconds(unixSecons * 1000); } // Creates a Date object from an instance of DOMHighResTimeStamp, returned by performance.now() for instance static fromHighResTimestamp(highResTimestamp) { return this.fromUnixMilliseconds(highResTimestamp + self.performance.timing.navigationStart) } // You don't usually need to call this in most cases, constructor uses MAX_AUX_UNIX_TIME static unix(unixTime) { return this.fromUnixSeconds(unixTime); } set(date) { date = this.constructor.toDate(date); this.setTime(date.setTime()); } clone() { return new this.constructor(this.getTime()); } toUnix() { return this.getTime() / 1000; } unix() { return Math.floor(this.toUnix()); } isBefore(date) { return this.getTime() < StemDate.toDate(date).getTime(); } equals(date) { return this.getTime() === StemDate.toDate(date).getTime(); } get(timeUnit) { timeUnit = TimeUnit.toTimeUnit(timeUnit); return timeUnit.getDateValue(this); } isSame(date, timeUnit) { if (!timeUnit) { return this.equals(date); } timeUnit = TimeUnit.toTimeUnit(timeUnit); date = this.constructor.toDate(date); let diff = this.diff(date); if (diff >= 2 * timeUnit) { // If the distance between the two dates is more than 2 standard lengths of the time unit // This would be wrong if you would have time unit that can sometimes last more than twice their canonical duration // Works correctly for all implemented time units return false; } return this.get(timeUnit) == date.get(timeUnit); } isAfter(date) { return this.getTime() > StemDate.toDate(date).getTime(); } isSameOrBefore(date) { return this.isBefore(date) || this.equals(date); } isSameOrAfter(date) { return this.isAfter(date) || this.equals(date); } isBetween(a, b) { return this.isSameOrAfter(a) && this.isSameOrBefore(b); } getWeekDay() { return this.getDay(); } addUnit(timeUnit, count = 1) { timeUnit = TimeUnit.toTimeUnit(timeUnit); count = parseInt(count); if (!timeUnit.isVariable()) { this.setTime(this.getTime() + timeUnit.getMilliseconds() * count); return this; } while (!timeUnit.dateMethodSuffix) { count *= timeUnit.multiplier; timeUnit = timeUnit.baseUnit; } const dateMethodSuffix = timeUnit.dateMethodSuffix; const currentValue = this["get" + dateMethodSuffix](); this["set" + dateMethodSuffix](currentValue + count); return this; } static min() { // TODO: simplify and remove code duplication let result = this.constructor.toDate(arguments[0]); for (let index = 1; index < arguments.length; index++) { let candidate = this.constructor.toDate(arguments[index]); if (candidate.isBefore(result)) { result = candidate; } } return result; } static max() { let result = this.constructor.toDate(arguments[0]); for (let index = 1; index < arguments.length; index++) { let candidate = this.constructor.toDate(arguments[index]); if (candidate.isAfter(result)) { result = candidate; } } return result; } // Assign the given date if current value if greater than it capUp(date) { date = this.constructor.toDate(date); if (this.isAfter(date)) { this.set(date); } } // Assign the given date if current value if less than it capDown(date) { date = this.constructor.toDate(date); if (this.isBefore(date)) { this.set(date); } } roundDown(timeUnit) { timeUnit = TimeUnit.toTimeUnit(timeUnit); // TODO: this is wrong for semester, etc, should be different then while (timeUnit = timeUnit.baseUnit) { this["set" + timeUnit.dateMethodSuffix](0); } return this; } roundUp(timeUnit) { const roundDown = this.clone().roundDown(timeUnit); if (this.equals(roundDown)) { this.set(roundDown); return this; } this.addUnit(timeUnit); return this.roundDown(timeUnit); } round(timeUnit) { let roundUp = this.clone().roundUp(timeUnit); let roundDown = this.clone().roundDown(timeUnit); // At a tie, preffer to round up, that's where time's going if (this.diff(roundUp) <= this.diff(roundDown)) { this.setTime(roundUp.getTime()); } else { this.setTime(roundDown.getTime()); } return this; } add(duration) { duration = Duration.toDuration(duration); if (duration.isAbsolute()) { this.setTime(this.getTime() + duration.toMilliseconds()); return this; } for (const key in duration) { const timeUnit = TimeUnit.CANONICAL[key]; if (timeUnit) { this.addUnit(timeUnit, duration[key]); } } return this; } subtract(duration) { duration = Duration.toDuration(duration).negate(); return this.add(duration); } diffDuration(date) { return new Duration(this.diff(date)); } // TODO: this should be a duration diff(date) { date = this.constructor.toDate(date); return Math.abs(+this - date); } // Just to keep moment compatibility, until we actually implement locales locale(loc) { return this; } static splitToTokens(str) { // TODO: "[HH]HH" will be split to ["HH", "HH"], so the escape does not solve the problem let tokens = []; let lastIsLetter = null; let escapeByCurlyBracket = false; let escapeBySquareBracket = false; for (let i = 0; i < str.length; i++) { let charCode = str.charCodeAt(i); if (charCode === 125 && escapeByCurlyBracket) { // '}' ending the escape escapeByCurlyBracket = false; lastIsLetter = null; } else if (charCode === 93 && escapeBySquareBracket) { // ']' ending the escape escapeBySquareBracket = false; lastIsLetter = null; } else if (escapeByCurlyBracket || escapeBySquareBracket) { // The character is escaped no matter what it is tokens[tokens.length - 1] += str[i]; } else if (charCode === 123) { // '{' starts a new escape escapeByCurlyBracket = true; tokens.push(""); } else if (charCode === 91) { // '[' starts a new escape escapeBySquareBracket = true; tokens.push(""); } else { let isLetter = (65 <= charCode && charCode <= 90) || (97 <= charCode && charCode <= 122); if (isLetter === lastIsLetter) { tokens[tokens.length - 1] += str[i]; } else { tokens.push(str[i]); } lastIsLetter = isLetter; } } if (escapeByCurlyBracket || escapeBySquareBracket) { console.warn("Unfinished escaped sequence!"); } return tokens; } evalToken(token) { let func = this.constructor.tokenFormattersMap.get(token); if (!func) { return token; } return func(this); } format(str="ISO") { let tokens = this.constructor.splitToTokens(str); tokens = tokens.map(token => this.evalToken(token)); return tokens.join(""); } isValid() { return (this.toString() !== "Invalid Date"); } utc() { // Temp hack return this.constructor.fromUnixMilliseconds(+this + this.getTimezoneOffset() * 60 * 1000); } isLeapYear() { let year = this.getFullYear(); return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); } daysInMonth() { // The 0th day of the next months is actually the last day in the current month let lastDayInMonth = new BaseDate(this.getFullYear(), this.getMonth() + 1, 0); return lastDayInMonth.getDate(); } getDaysCountPerMonth(index) { const months = [31, 28 + this.isLeapYear(), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; return months[index]; } getDayInYear() { const month = this.getMonth(); let totalDays = 0; for (let i = 0; i < month; i += 1) { totalDays += this.getDaysCountPerMonth(i); } return totalDays + this.getDate() - 1; } getWeekInYear() { return (this.getDayInYear() - this.getWeekDay()) / 7; } } Duration.prototype.format = function (pattern) { return StemDate.fromUnixMilliseconds(this.toMilliseconds()).utc().format(pattern); }; const miniWeekDays = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; const shortWeekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const longWeekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const shortMonths = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const longMonths = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; class DateLocale { constructor(obj={}) { Object.assign(this, obj); this.formats = this.formats || {}; } getFormat(longDate) { return this.formats[longDate]; } setFormat(pattern, func) { this.formats[pattern] = func; } format(date, pattern) { return date.format(pattern); } getRelativeTime() { throw Error("Not implemented"); } } StemDate.tokenFormattersMap = new Map([ ["ISO", date => date.toISOString()], ["Y", date => date.getFullYear()], ["YY", date => padNumber(date.getFullYear() % 100, 2)], ["YYYY", date => date.getFullYear()], ["M", date => (date.getMonth() + 1)], ["MM", date => padNumber(date.getMonth() + 1, 2)], ["MMM", date => shortMonths[date.getMonth()]], ["MMMM", date => longMonths[date.getMonth()]], ["D", date => date.getDate()], ["Do", date => suffixWithOrdinal(date.getDate())], ["DD", date => padNumber(date.getDate(), 2)], ["d", date => date.getWeekDay()], ["do", date => suffixWithOrdinal(date.getWeekDay())], ["dd", date => miniWeekDays[date.getWeekDay()]], ["ddd", date => shortWeekDays[date.getWeekDay()]], ["dddd", date => longWeekdays[date.getWeekDay()]], ["H", date => date.getHours()], ["HH", date => padNumber(date.getHours(), 2)], ["h", date => date.getHours() % 12 ? date.getHours() % 12 : 12], ["hh", date => padNumber(date.getHours() % 12 ? date.getHours() % 12 : 12, 2)], ["m", date => date.getMinutes()], ["mm", date => padNumber(date.getMinutes(), 2)], ["s", date => date.getSeconds()], ["ss", date => padNumber(date.getSeconds(), 2)], ["S", date => Math.floor(date.getMilliseconds() / 100)], ["SS", date => padNumber(Math.floor(date.getMilliseconds() / 10), 2)], ["SSS", date => padNumber(date.getMilliseconds(), 3)], ["ms", date => padNumber(date.getMilliseconds(), 3)], ["aa", date => date.getHours() > 12 ? "pm" : "am"], ["AA", date => date.getHours() > 12 ? "PM" : "AM"], ["LL", date => date.format("MMMM Do, YYYY")], ]); let Date = StemDate; export {Date, StemDate};