Created
September 24, 2018 20:27
-
-
Save MaxGabriel/3b56aa8766b64281541587cd35ee04df to your computer and use it in GitHub Desktop.
Date parsing code
This file contains 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
import * as P from 'parsimmon' | |
import Day from '~/utils/Day' | |
import Month from '~/utils/Month' | |
const shortMonths = { | |
jan: 1, | |
feb: 2, | |
mar: 3, | |
apr: 4, | |
may: 5, | |
jun: 6, | |
jul: 7, | |
aug: 8, | |
sep: 9, | |
sept: 9, // Sept is also acceptable | |
oct: 10, | |
nov: 11, | |
dec: 12, | |
} | |
const longMonths = { | |
january: 1, | |
february: 2, | |
march: 3, | |
april: 4, | |
may: 5, | |
june: 6, | |
july: 7, | |
august: 8, | |
september: 9, | |
october: 10, | |
november: 11, | |
december: 12, | |
} | |
/** If the current month is e.g. July and the user types Dec 12, give them Dec 12 of the previous year | |
*/ | |
const yearForMonth = (aMonth: number): number => { | |
const currentMonth = Month.currentMonth().month | |
const currentYear = Month.currentMonth().year | |
const monthIsInFuture = aMonth > currentMonth | |
return monthIsInFuture ? currentYear - 1 : currentYear | |
} | |
/** When the user doesn't enter the day and a default one is chosen, should it be the first or last day of the month? */ | |
export enum DayDefault { | |
Start, | |
End, | |
} | |
// This file uses parser combinators for parsing, using Parsimmon | |
// Note: some Parsimmon functions (e.g. createLanguage) don't generate types, so I'm using uglier ones. | |
// When the user is entering dates, we want to jump the calendar to the date they've entered | |
// A problem is that if we jumped on every date we'd accept, we'd jump all the time | |
// because e.g. just '1' or '1 1' is a valid date, so we'd jump a bunch when typing '1 1 2012' | |
// Thus there are two main parsers: ambiguous and unambiguous | |
// Ambiguous is used while typing, unambiguous is used when you blur the input field | |
// All supported formats | |
export const ambiguousDayParser = (dayDefault: DayDefault): P.Parser<Day> => { | |
return P.alt( | |
wordDay(), | |
monthDayYear(), | |
monthDay(), | |
monthYear(dayDefault), | |
justMonth(dayDefault) | |
) | |
} | |
// Only formats that are unambiguously complete | |
// E.g. '1 1' is accepted by the ambiguous parser as the current year, | |
// but could be '1 1 2012' | |
export const unambiguousDayParser = (dayDefault: DayDefault): P.Parser<Day> => { | |
return P.alt(wordDay(), monthDayFullYear(), monthFullYear(dayDefault)) | |
} | |
// Combos | |
const monthDayYear = (): P.Parser<Day> => { | |
return P.seq(month(), separator(), numberDay(), separator(), year()).map( | |
([aMonth, _s1, aDay, _s2, aYear]) => { | |
return new Day(aYear, aMonth, aDay) | |
} | |
) | |
} | |
const monthDayFullYear = (): P.Parser<Day> => { | |
return P.seq(month(), separator(), numberDay(), separator(), fullYear()).map( | |
([aMonth, _s, aDay, _s2, aYear]) => { | |
return new Day(aYear, aMonth, aDay) | |
} | |
) | |
} | |
const monthDay = (): P.Parser<Day> => { | |
return P.seq(month(), separator(), numberDay()).map(([aMonth, _s, day]) => { | |
const aYear = yearForMonth(aMonth) | |
return new Day(aYear, aMonth, day) | |
}) | |
} | |
// Words | |
const wordDay = (): P.Parser<Day> => { | |
return P.alt(today(), yesterday()) | |
} | |
const today = (): P.Parser<Day> => { | |
return P.regexp(/^today$/i).map(() => { | |
return Day.today() | |
}) | |
} | |
const yesterday = (): P.Parser<Day> => { | |
return P.regexp(/^yesterday$/i).map(() => { | |
return Day.yesterday() | |
}) | |
} | |
// Months | |
const month = (): P.Parser<number> => { | |
return P.alt(numberMonth(), shortMonth(), longMonth()) | |
} | |
const numberMonth = (): P.Parser<number> => { | |
return P.regexp(/[0-9]+/) | |
.map(s => Number(s)) | |
.chain(n => { | |
if (n >= 1 && n <= 12) { | |
return P.succeed(n) | |
} else { | |
return P.fail('Month must be between 1 and 12') | |
} | |
}) | |
} | |
const shortMonth = (): P.Parser<number> => { | |
return P.letters.chain(s => { | |
const n = shortMonths[s.toLowerCase()] | |
if (n) { | |
return P.succeed(n) | |
} else { | |
return P.fail('Not a valid short month') | |
} | |
}) | |
} | |
const longMonth = (): P.Parser<number> => { | |
return P.letters.chain(s => { | |
const n = longMonths[s.toLowerCase()] | |
if (n) { | |
return P.succeed(n) | |
} else { | |
return P.fail('Not a valid month') | |
} | |
}) | |
} | |
// Day | |
const numberDay = (): P.Parser<number> => { | |
return P.regexp(/[0-9]+/) | |
.map(s => Number(s)) | |
.chain(n => { | |
return numberDaySuffix() | |
.fallback('') | |
.chain(() => { | |
if (n > 0 && n <= 31) { | |
return P.succeed(n) | |
} else { | |
return P.fail('Day must be between 1 and 31') | |
} | |
}) | |
}) | |
} | |
const numberDaySuffix = (): P.Parser<string> => { | |
return P.alt(P.string('st'), P.string('nd'), P.string('rd'), P.string('th')) | |
} | |
// Year | |
const year = (): P.Parser<number> => { | |
return P.regexp(/[0-9]+/) | |
.map(s => Number(s)) | |
.chain(n => { | |
if (n > 999 && n <= 9999) { | |
return P.succeed(n) | |
} else if (n > 30 && n < 99) { | |
return P.succeed(1900 + n) | |
} else if (n >= 0 && n <= 30) { | |
return P.succeed(2000 + n) | |
} else { | |
return P.fail('Invalid year') | |
} | |
}) | |
} | |
const fullYear = (): P.Parser<number> => { | |
return P.regexp(/[0-9]+/) | |
.map(s => Number(s)) | |
.chain(n => { | |
if (n > 999 && n <= 9999) { | |
return P.succeed(n) | |
} else { | |
return P.fail('Invalid year') | |
} | |
}) | |
} | |
const separator = (): P.Parser<string[]> => { | |
return P.oneOf(',-/ .').many() | |
} | |
const justMonth = (dayDefault: DayDefault): P.Parser<Day> => { | |
return month().map(aMonth => { | |
const day = 1 | |
const aYear = yearForMonth(aMonth) | |
const firstDay = new Day(aYear, aMonth, day) | |
return dayDefault === DayDefault.Start | |
? firstDay | |
: firstDay.toMonth().toLastDay() | |
}) | |
} | |
const monthYear = (dayDefault: DayDefault): P.Parser<Day> => { | |
return P.seq(month(), separator(), year()).map(([aMonth, _s, aYear]) => { | |
const firstDay = new Day(aYear, aMonth, 1) | |
return dayDefault === DayDefault.Start | |
? firstDay | |
: firstDay.toMonth().toLastDay() | |
}) | |
} | |
const monthFullYear = (dayDefault: DayDefault): P.Parser<Day> => { | |
return P.seq(month(), separator(), fullYear()).map(([aMonth, _s, aYear]) => { | |
const firstDay = new Day(aYear, aMonth, 1) | |
return dayDefault === DayDefault.Start | |
? firstDay | |
: firstDay.toMonth().toLastDay() | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment