Skip to content

Instantly share code, notes, and snippets.

@dead-claudia
Last active September 27, 2018 17:33
Show Gist options
  • Save dead-claudia/4aa871f66ea0cda585bcb4c7e4cb9540 to your computer and use it in GitHub Desktop.
Save dead-claudia/4aa871f66ea0cda585bcb4c7e4cb9540 to your computer and use it in GitHub Desktop.
Simple date library
// 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