Last active
September 27, 2018 17:33
-
-
Save dead-claudia/4aa871f66ea0cda585bcb4c7e4cb9540 to your computer and use it in GitHub Desktop.
Simple date library
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A simpler implementation of date handling, with the following additional | |
// assumptions: | |
// | |
// 1. We only need the day, month, and year. | |
// 2. Time zones are irrelevant. | |
// 3. Day/month/year checks and range checks are *far* more common than date | |
// difference calculation. | |
// 4. Dates are rarely written to, mostly read. | |
// | |
// Helpfully, this lets us represent dates with a single 32-bit integer, which | |
// makes handling these dates relatively lightweight. | |
// | |
// Here's how the fields are stored, from low to high offset: | |
// | |
// - Day: 8-bit, 1 to 31 (clamped to this range) | |
// - Month: 4-bit, 1 to 12 (clamped to this range) | |
// - Year: 20-bit, -1048576 to 1048575 (larger than JS) | |
// | |
// Conveniently, this also ensures proper comparison ordering and compatibility | |
// with `arr.sort((a, b) => a - b)`. | |
// | |
// Additional notes: | |
// | |
// - This returns an immutable data structure. Instances should be considered | |
// primitive numeric subtypes. | |
// - Months start from 1, *unlike* in JS. | |
// - Weeks start from 1, *unlike* in JS. | |
// - There is no way to get the number of days/millis from an arbitrary date. | |
// - Like in JS, this does properly wrap invalid days like in | |
// `create(2017, 5, 39) === create(2017, 6, 8)` or | |
// `create(2017, 5, 0) === create(2017, 4, 30)` | |
// | |
// Here's the API: | |
// | |
// ```js | |
// import * as LD from "./local-date" | |
// | |
// LD.create(year, month, day) // Create a new date from a given year/month/day | |
// LD.getYear(date) // Get this date's year | |
// LD.getMonth(date) // Get this date's month | |
// LD.getDay(date) // Get this date's day of month | |
// LD.getWeek(date) // Get this date's day of week | |
// LD.setYear(date, year) // Set this date's year | |
// LD.setMonth(date, month) // Set this date's month | |
// LD.setDay(date, day) // Set this date's day of month | |
// LD.fromDate(dateObj) // Create a new date from a `Date` object. | |
// LD.toDate(date) // Create a new `Date` object from this date. | |
// | |
// date > other; date < other // Compare two dates relationally | |
// date === other // Compare two dates for (in)equality | |
// ``` | |
function mod(a, b) { | |
return (a % b + b) % b | |
} | |
const DaysInMonthTable = new Uint8Array([ | |
// Normal year | |
0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 0, 0, 0, | |
// Leap year | |
0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 0, 0, 0, | |
]) | |
function isLeap(year) { | |
year |= 0 | |
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) | |
} | |
export function create(year, month, day) { | |
year |= 0; month |= 0; day |= 0 | |
if (month < 1 || month > 12) { | |
year += month / 12 | 0 | |
month = month % 12 | 0 | |
if (month <= 0) month = 11 - month | |
} | |
const maxDay = DaysInMonthTable[month + (isLeap(year) << 4)] | |
month += day / maxDay | 0 | |
day = day % maxDay | 0 | |
if (day < 0) day = maxDay - day | |
if (month < 1 || month > 12) { | |
year += month / 12 | 0 | |
month = month % 12 | 0 | |
if (month <= 0) month = 11 - month | |
} | |
return year << 12 | month << 8 | day | |
} | |
export function getYear(date) { | |
// Note: the sign *must* be extended when returning the year. | |
return (date & 0xfffff000) >> 12 | |
} | |
export function getMonth(date) { | |
return (date & 0x00000f00) >>> 8 | |
} | |
export function getDay(date) { | |
return date & 0x000000ff | |
} | |
// This uses the Doomsday rule to calculate the day of week. | |
// https://en.wikipedia.org/wiki/Doomsday_rule | |
const DoomsdayDayTable = new Uint8Array([ | |
// Normal | |
0, 3, 28, 0, 4, 9, 6, 11, 8, 5, 10, 7, 12, 0, 0, 0, | |
// Leap year | |
0, 4, 29, 0, 4, 9, 6, 11, 8, 5, 10, 7, 12, 0, 0, 0, | |
]) | |
export function getWeek(date) { | |
const year = getYear(date) | |
const month = getMonth(date) | |
const day = getDay(date) | |
// The algorithm assumes 0 = Sunday, we're exposing 1 for that. | |
return 1 + mod( | |
// Calculate date difference | |
day - DoomsdayDayTable[month + (isLeap(year) << 4)] + | |
// Calculate doomsday weekday | |
mod( | |
2 /* Tuesday */ + | |
5 * mod(year, 4) + | |
4 * mod(year, 100) + | |
6 * mod(year, 400), | |
7 | |
), | |
7 | |
) | |
} | |
export function setYear(date, year) { | |
return date & 0x00000fff | year << 12 | |
} | |
export function setMonth(date, month) { | |
if (month < 1 || month > 12) { | |
date += month / 12 << 12 | |
month = month % 12 | 0 | |
if (month <= 0) month = 11 - month | |
} | |
return date & 0xfffff0ff | month << 8 | |
} | |
export function setDay(date, day) { | |
let year = getYear(date) | |
let month = getMonth(date) | |
const maxDay = DaysInMonthTable[month + (isLeap(year) << 4)] | |
month += day / maxDay | 0 | |
day = day % maxDay | 0 | |
if (day < 0) day = maxDay - day | |
if (month < 1 || month > 12) { | |
year += month / 12 | 0 | |
month = month % 12 | 0 | |
if (month <= 0) month = 11 - month | |
} | |
return year << 12 | month << 8 | day | |
} | |
export function fromDate(dateObj) { | |
return create( | |
dateObj.getFullYear(), | |
dateObj.getMonth() + 1, | |
dateObj.getDate() | |
) | |
} | |
export function toDate(date) { | |
// 1. `new Date(0, ...)` is equivalent to `new Date(1900, ...)`, so we can't | |
// use the constructor directly. | |
// | |
// 2. `new Date(0)` and `new Date(0, 0, 0)` evaluate to December 31. This is | |
// a problem when we go to set the month, since if we set the month to | |
// one that doesn't have 31 days in it, it wraps around on us (e.g. | |
// February 31 becomes March 3). That screws up our month count, so we | |
// have to explicitly construct January 1, 1900 to avoid this. | |
const dateObj = new Date(0, 0, 1) | |
dateObj.setFullYear(getYear(date)) | |
dateObj.setMonth(getMonth(date) - 1) | |
dateObj.setDate(getDay(date)) | |
return dateObj | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment