Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@MaxGabriel
Created September 24, 2018 20:27
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save MaxGabriel/3b56aa8766b64281541587cd35ee04df to your computer and use it in GitHub Desktop.
Save MaxGabriel/3b56aa8766b64281541587cd35ee04df to your computer and use it in GitHub Desktop.
Date parsing code
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