Skip to content

Instantly share code, notes, and snippets.

@dwachss
Last active February 1, 2016 14:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dwachss/4f9a6c77c8feb8e2ad09 to your computer and use it in GitHub Desktop.
Save dwachss/4f9a6c77c8feb8e2ad09 to your computer and use it in GitHub Desktop.
GDate API: generalized date object that allows for multiple calendar systems

#GDate

##Rationale Globalize as it stands (Version 1.0.0) assumes that dates are to be formatted and parsed using the Gregorian calendar. The CLDR database, however, includes nomenclature for other calendar systems. jquery/globalize#320 is the attempt to support those other calendars.

To make that possible, GDate is the proposed interface that abstracts the necessary calendar calculations, and that every supported calendar system should implement.

##Structure of a Generalized Date The CLDR json data assumes the following for a date in a given calendar system:

  • Every day is uniquely defined by five parameters: era, year, month, date and monthtype, though there may be days that are not defined in a given calendar (for instance, in the Jewish calendar, dating starts at 7 October 3761 BCE and days before this are undefined).
  • era is a number, the index to the names of the "eras"--sets of years such as BC and AD. They are numbered consecutively from 0.
  • year is a positive number. It should not be assumed that years are consecutive; for instance, in the Gregorian calendar in era 0 (BC), years run backward. 1000 BC is before 999 BC.
  • month is a number. In most calendar systems, the numbers run consecutively from 1. Thus the Gregorian months are [1, 2, 3, ...], but this is not always the case. Calendar systems that use leap months intercalate extra months, and the way that CLDR encodes this is not consistent. The JSON encoding of the CLDR data that is used in Globalize for the Hebrew calendar uses 7 as the leap month, so it is present only in leap years. It cannot be assumed that months go in numerical order; in a Hebrew non-leap year, month===5 is followed by month===7. In the Chinese calendar, the leap month has the same number as the preceding month, but with a different monthType.
  • monthType is undefined or a string. It is used for lunisolar calendars that may intercalate a leap month. Most months will have this be undefined but leap months have 'leap'. The Hindu calendar is not yet implemented as of CLDR 27, but is planned to also have month types of 'standardAfterLeap' and 'combined' (since it uses a more complex method of naming leap months).
  • date is a positive number that runs from 1 to the last day of the month.
  • Days of the week all use the same 7-day calendar as the Gregorian calendar (the names will of course be different depending on the calendar and the language). They are indexed not by number but by abbreviation: ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].

##API ###constructors GDate is meant to be an abstract class from which the calendar algorithms inherit. For the definitions here, I will use gdate = new GDate. The created object, gdate is immutable, unlike a JavaScript Date object. ####gdate = new GDate(d) Create a new GDate corresponding to the JavaScript Date d. If d is not defined in the calendar, mark gdate as invalid. ####gdate = new GDate(g) Create a new GDate from an existing GDate g. Must be the same as gdate = new GDate( g.toDate() ). ####gdate = new GDate (era, year, month, date, monthType)Create a newGDatewith the parameters given. As discussed above,era, year, monthanddateare numbers;monthTypeisundefinedor a string. The following algorithm is used forundefinedornull` parameters:

If all the parameters are undefined, then return new GDate( new Date() ). In other words, use today's date. Thus new GDate() corresponds to today's date.
If date is undefined, then, if any of [year, month] are not undefined, set date = 1. Otherwise, use today's date.
If month is undefined, then, if year is not undefined, set month = 1 (assume that this is the first month). Otherwise, use today's month.
If year is undefined, then use today's year. Note that if era is defined and year is not, the results may be unexpected.
If era is undefined, use today's era.
If the era does not correspond to a valid era name, the implementation may attempt to guess the correct era. Otherwise, mark the gdate invalid.
If the month does not correspond to a valid month number, the implementation may attempt to guess the correct month. Otherwise, mark the gdate invalid. Similarly, if monthType is not a valid type or does not correspond to the given month and year, the implementation may assign an appropriate month type or mark the gdate invalid.
If the date is out of bounds for the month, coerce it into bounds (if date is < 1, set date = 1; if date > last date in month, set date = last date in month).
If the year is out of bounds for the era, coerce it into bounds.

###Getters ####gdate.getEra() Returns the era; a number or NaN if gdate is invalid. ####gdate.getYear() Returns the year; a number or NaN if gdate is invalid. ####gdate.getMonth() Returns the month; a number or NaN if gdate is invalid. ####gdate.getMonthType() Returns the month type; undefined or a string, or undefined if gdate is invalid. ####gdate.getDate() Returns the date; a number or NaN ifgdate is invalid. ####gdate.toDate() Returns the JavaScript Date object corresponding to gdate, or new Date(NaN)ifgdate is invalid.

###Calculations ####gdate.nextDate( n ) Returns a new GDate using the same calendar, but corresponding to the day n days in the future. Negative n for days in the past are acceptable. If arguments.length === 0 set n = 1. ####gdate.nextMonth( n ) Returns a new GDate using the same calendar, but corresponding to the day n months in the future. Negative n for months in the past are acceptable. If arguments.length === 0 set n = 1. The date is kept as gdate.getDate() but if that date does not exist (the new month is too short) use the last day of the month. ####gdate.nextYear( n ) Returns a new GDate using the same calendar, but corresponding to the day n years in the future. Negative n for years in the past are acceptable. If arguments.length === 0 set n = 1. ####gdate.startOfMonth() Returns a new GDate using the same calendar, corresponding the first day of the month of gdate. ####gdate.startOfYear() Returns a new GDate using the same calendar, corresponding the first day of the year of gdate.

###The month string The CLDR Javascript database uses a variety of nomenclatures for leap months (in the Hebrew and Chinese calendars; the Hindu calendar is not implemented as of version 27). GDate standardizes it into a number and possibly a type, which will be one of "leap", "standardAfterLeap" or "combined". See http://www.unicode.org/reports/tr35/tr35-dates.html#monthPatterns_cyclicNameSets .

##Notes This is very much a work in progress; comments are always welcome.

There is a concept of an "extended year" that incorporates both era and year. For instance, Date.getFullYear() will return a negative number for BCE years, and the Chinese calendar which generally has a 60-year cycle can also date years from 2637 BCE. This is not yet incorporated into GDate (so the u date field in CLDR is not defined) but could be.

There is no nextEra or startOfEra. Is there a use case for those? Specifically startOfEra: in the constructor, if era is defined but year is not, we may want to use the first day of the era, with the possibility that such a day is not defined (BC in the Gregorian calendar has no first day).

The nomenclature has room for improvement. nextDate() etc. should probably be addDate(). Since GDates are immutable, getDate() etc. could be shortened to date(), or, if we don't need to support IE8, use object.defineProperty() and create read-only properties like gdate.date.

getDate and toDate are confusing. One "date" refers to the day number, the other to a Javascript Date object. toDate should probably be toJavascriptDate.

@rxaviers
Copy link

Awesome! It's looking really great! A couple of comments below...

If month is undefined, then, if any of [era, year] are not undefined, use the month of the first day of the most specific period (year > era) that is not undefined. Otherwise, use today's month.

Can you give me an example of the input for "Otherwise, use today's month"? Is it GDate(undefined, undefined, undefined, date) // where date !== undefined.

If the date is out of bounds for the month, coerce it into bounds (if date is < 1, set date = 1; if date > last date in month, set date = last date in month).

This goes against native Date behaviour, which extrapolates months according to the out of boundary offset. For example:

new Date(2015, 1, 29);
// > Sun Mar 01 2015 00:00:00 GMT-0300 (BRT)

Anyway, I miss knowing where any field is out of boundaries, so Globalize can throw an Error when that happens (instead of formatting something else). There could be an option to change GDate's behaviour for whether coercing or not.

gdate.getMonth()

Returns the month; a string or undefined if gdate is invalid.

Not returning a Number similar to the other methods seems incoherent.

I know you're using String, because CLDR index for months are Strings. But, what about having .getMonthName() (or a better name for this method) for the string (e.g., "7-yeartype-leap") while keeping getMonth() for numeric (e.g., 7). This will make Globalize implementation (https://github.com/jquery/globalize/pull/447/files#diff-3372b7628d9709f40d171d30a27339feR112) simpler.

@dwachss
Copy link
Author

dwachss commented Jul 28, 2015

Per your comments:

  1. Yes, an undefined month would be new GDate (undefined, undefined, undefined, date) corresponds to date in today's month.
  2. I realize that coercing into bounds is different from native Date, but it makes things like nextMonth much easier (since then you would want to coerce). The use case for Date's behavior is to get things like the last day of a month (with new Date (year, month+1, 0)) but I'd rather have an explicit gdate.nextMonth().startOfMonth().nextDay(-1). I want date parsing to be as lenient as possible, so I don't want GDate to throw any errors. Checking that gdate.getDate() === date should detect any out-of-bounds conditions if you want them.
  3. We can change the name of getMonth but returning a number would be wrong, too likely to produce errors. I would rather have the user explicitly do parseInt (gdate.getMonth(), 10).
    I'm going to add documentation on the format of the month string.

@dwachss
Copy link
Author

dwachss commented Oct 19, 2015

rxaviers:
In trying to implement the Chinese calendar, I realized that you are right about the months. I have changed the interface to have month be a number (so "normal" calendars don't have to worry about strings) and added a "monthType" parameter that uses the terms from the CLDR monthPattern element ( http://www.unicode.org/reports/tr35/tr35-dates.html#monthPatterns_cyclicNameSets ).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment