diff --git a/directives.md b/directives.md index 8170200..c7c7bda 100644 --- a/directives.md +++ b/directives.md @@ -16,8 +16,8 @@ The only cases in which a directive with any or no modifier cannot parse a timestamp produced with the output of any other modifier (i.e. "%d %m %Y" successfully parses the output of "-d %_m %Y") are: -- `:` is used to produce an ordinal number; e.g. only `%:d` can parse the output of `%:d`. -- `^` is used to produce an unsigned year. Only `%^Y` can correctly parse the output of `%^Y`, and then only when an era directive `%#` is also present in the timestamp. +- `:` is used to produce an ordinal number; e.g. `%:d` can parse the output of `%:d` but not the output of `%d`, `%-d`, or `%_d`. Neither can these differently-modified directives parse the output of `%:d`. +- `^` is used to produce an unsigned year. Either `%Y` or `%^Y` can correctly parse the output of `%^Y`, but only when an era directive `%#` is also present in the timestamp. ## Overview @@ -31,7 +31,7 @@ timestamp produced with the output of any other modifier - `%D` date; same as `%m/%d/%y` - `%e` space-padded numeric day of the month; same as `%_d` - `%f` microsecond in second (000000-999999) -- `%F` ISO 8601 format; same as `%Y-%m-%d` +- `%F` ISO 8601 date format; same as `%Y-%m-%d` - `%g` two-digit ISO 8601 week year, i.e. "18" when the ISO week year is "2018" (00-99) - `%G` full ISO 8601 week year - `%h` abbreviated month name; same as `%b` @@ -68,3 +68,317 @@ timestamp produced with the output of any other modifier - `%+` date; same as "%a %b %e %H:%M:%S %Z %Y" - `%#` era name, i.e. "CE" or "BCE" - `%%` literal "%" character + +## Details + +### Abbreviated weekday name %a + +Abbreviated weekday name. Using the default English settings these names are "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", and "Sat". + +The weekday names can be changed by passing an options object to **strftime** or **strptime** which includes a `shortWeekdayNames` attribute. The value of this attribute must be an array containing seven abbreviated weekday names, beginning with Sunday and ending with Saturday. + +The modified directive `%^a` produces switched-case outputs. With the default English settings, this means the output will be "SUN", "MON", "TUE", and so on. + +The `%a` directive, when used in a format for **strptime**, is also able to parse the outputs of the full weekday name directive `%A`. Parsing is case-insensitive. This means that, for example, the string "mOnDay" will be correctly parsed no matter whether the parsing directive is modified or not. + +### Full weekday name %A + +Full weekday name. Using the default English settings these names are "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", and "Saturday". + +The weekday names can be changed by passing an options object to **strftime** or **strptime** which includes a `longWeekdayNames` attribute. The value of this attribute must be an array containing seven full weekday names, beginning with Sunday and ending with Saturday. + +The modified directive `%^A` produces switched-case outputs. With the default English settings, this means the output will be "SUNDAY", "MONDAY", "TUESDAY", and so on. + +The `%A` directive, when used in a format for **strptime**, is also able to parse the outputs of the abbreviated weekday name directive `%a`. Parsing is case-insensitive. This means that, for example, the string "mOnDay" will be correctly parsed no matter whether the parsing directive is modified or not. + +### Abbreviated month name %b + +Abbreviated month name. Using the default English settings these names are "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", and "Dec". + +The month names can be changed by passing an options object to **strftime** or **strptime** which includes a `shortMonthNames` attribute. The value of this attribute must be an array containing twelve abbreviated month names, beginning with January and ending with December. + +The modified directive `%^b` produces switched-case outputs. With the default English settings, this means the output will be "JAN", "FEB", "MAR", and so on. + +The `%b` directive, when used in a format for **strptime**, is also able to parse the outputs of the full month name directive `%B`. Parsing is case-insensitive. This means that, for example, the string "jANuARy" will be correctly parsed no matter whether the parsing directive is modified or not. + +### Full month name %B + +Full month name. Using the default English settings these names are "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", and "December". + +The month names can be changed by passing an options object to **strftime** or **strptime** which includes a `longMonthNames` attribute. The value of this attribute must be an array containing twelve full month names, beginning with January and ending with December. + +The modified directive `%^B` produces switched-case outputs. With the default English settings, this means the output will be "JANUARY", "FEBRUARY", "MARCH", and so on. + +The `%B` directive, when used in a format for **strptime**, is also able to parse the outputs of the abbreviated month name directive `%b`. Parsing is case-insensitive. This means that, for example, the string "jANuARy" will be correctly parsed no matter whether the parsing directive is modified or not. + +### Date and time %c + +This directive is rewritten as `%a %b %e %H:%M:%S %Y`. + +### Century %C + +Century number, e.g. `20` when the year is `2018`. The number is padded with zeroes if it is fewer than two digits long. In most practical use cases, this number will be either 19 or 20. However, it is possible for the century number to be outside that range, or even for it to be negative. + +Typically, this directive is used in combination with `%y`, the two-digit year directive. It is not safe to use this directive in combination with the ISO week year directive `%g`; doing so will cause the information in the `%C` century directive to be ignored. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Day of the month %d + +Day of the month. The number is padded with zeroes if it is fewer than two digits long. The day number is always in the range 01 through 31. + +The day of the month can be written using either the `%d` or `%e` directives. Either directive is able to parse day number output written using the other directive. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Date %D + +This directive is rewritten as `%m/%d/%y`. + +### Space-padded day of the month %e + +Day of the month. The number is padded with spaces if it is fewer than two digits long. The day number is always in the range 1 through 31. + +The day of the month can be written using either the `%d` or `%e` directives. Either directive is able to parse day number output written using the other directive. + +This numeric directive can be modified using the no-padding `-` and ordinal `:` modifiers. To get the day number padded with zeroes instead of with spaces, use the `%d` directive instead. + +### Microsecond %f + +Microsecond in second. The number is padded with zeroes if it is fewer than six digits long. The microsecond number is always in the range 000000 through 999999. + +Realistically, since at the time of writing JavaScript Date objects do not offer any better than millisecond precision, writing a timestamp with a microsecond directive will give only three significant digits followed by zeroes (e.g. `123000` would be written) and parsing a timestamp with a microsecond `%f` directive will erase the trailing three significant digits. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### ISO 8601 date format %F + +This directive is rewritten as `%Y-%m-%d`. + +### Two-digit ISO 8601 week year %g + +Two-digit ISO week year, e.g. `18` when the ISO week year is `2018`. The number is padded with zeroes if it is fewer than two digits long. Normally, this directive should be used in combination with the ISO week number directive `%V` and the weekday number directive `%u`. Please note that the ISO week year may differ from the calendar year `%y`, `%Y` for days of the year before January 4th. + +When parsing a two-digit ISO week year, numbers less than or equal to 68 are considered to belong to the 21st century (e.g. `2068`) whereas numbers greater than 68 are considered to belong to the 20th century (e.g. `1969`). To avoid the possibility of reading the wrong year number from a timestamp outside the range 1969-2068, it is better to always write timestamps using the full ISO year directive `%G` instead of the two-digit year `%g`. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +You can refer to the [ISO week date article on Wikipedia](https://en.wikipedia.org/wiki/ISO_week_date) for more information about ISO 8601 week dates. + +### Full ISO 8601 week year %G + +Full ISO week year. The number is padded with zeroes if it is fewer than four digits long. Normally, this directive should be used in combination with the ISO week number directive `%V` and the weekday number directive `%u`. Please note that the ISO week year may differ from the calendar year `%y`, `%Y` for days of the year before January 4th. Note also that it is possible for the ISO week year to be negative. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +You can refer to the [ISO week date article on Wikipedia](https://en.wikipedia.org/wiki/ISO_week_date) for more information about ISO 8601 week dates. + +### Abbreviated month name %h + +This directive is identical to the [abbreviated month name `%b` directive](#user-content-abbreviated-month-name-b). + +### Hour number (24-hour) %H + +Hour number on a 24-hour clock. The number is padded with spaces if it is fewer than two digits long. The hour number is always in the range 00 through 23. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Hour number (12-hour) %I + +Hour number on a 12-hour clock. The number is padded with spaces if it is fewer than two digits long. The hour number is always in the range 01 through 12. Normally, this directive should be used in combination with the AM/PM directive `%p` or `%P`. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Day of the year %j + +Ordinal day number in the year. The number is padded with spaces if it is fewer than three digits long. The day number is always in the range 000 through 366. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Hour number (24-hour) %k + +This directive is identical to the [hour number (24-hour) `%H` directive](#user-content-hour-number-24-hour-h). + +### Hour number (12-hour) %l + +This directive is identical to the [hour number (12-hour) `%I` directive](#user-content-hour-number-12-hour-i). + +### Millisecond %L + +Millisecond in second. The number is padded with zeroes if it is fewer than three digits long. The microsecond number is always in the range 000 through 999. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Month number %m + +Month number. The number is padded with spaces if it is fewer than two digits long. The month number is always in the range 01 through 12. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Minute %M + +Minute number. The number is padded with spaces if it is fewer than two digits long. The minute number is always in the range 00 through 59. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Newline character %n + +This directive literally writes the newline character `\n` when formatting a date with **strftime** and matches the same newline character when parsing a timestamp using **strptime**. + +### Uppercase AM/PM %p + +Uppercase AM/PM text. Using the default English settings these strings are "AM" for times before noon and "PM" for noon and times after noon but before midnight. Normally, this directive is used in combination with the 12-hour hour number directive `%I`. + +The AM/PM strings can be changed by passing an options object to **strftime** or **strptime** which includes a `meridiemNames` attribute. The value of this attribute must be an array two elements, an AM equivalent and then a PM equivalent. The strings given here are written with their original capitalization when using this `%p` directive. + +The modified directive `%^p` produces switched-case outputs. With the default English settings, this means the output will be "am" or "pm". Note that, with the default English settings, this is the same result as using the lowercase am/pm `%P` directive. + +The `%p` directive, when used in a format for **strptime**, is also able to parse the outputs of the lowercase am/pm directive `%P`. Parsing is case-insensitive. This means that, for example, the string "Pm" will be correctly parsed no matter whether the parsing directive is modified or not. + +### Lowercase am/pm %P + +Lowercase am/pm text. Using the default English settings these strings are "am" for times before noon and "pm" for noon and times after noon but before midnight. Normally, this directive is used in combination with the 12-hour hour number directive `%I`. + +The am/pm strings can be changed by passing an options object to **strftime** or **strptime** which includes a `meridiemNames` attribute. The value of this attribute must be an array two elements, an AM equivalent and then a PM equivalent. The strings given here are written after conversion to lowercase when using this `%P` directive. + +The modified directive `%^P` produces switched-case outputs. With the default English settings, this means the output will be "AM" or "PM". Note that, with the default English settings, this is the same result as using the uppercase AM/PM `%p` directive. + +The `%P` directive, when used in a format for **strptime**, is also able to parse the outputs of the uppercase AM/PM directive `%o`. Parsing is case-insensitive. This means that, for example, the string "Pm" will be correctly parsed no matter whether the parsing directive is modified or not. + +### Microseconds since epoch %Q + +The number of microseconds that have elapsed since [00:00:00 on 1 January 1970](https://en.wikipedia.org/wiki/Unix_time) in the timezone that the timestamp applies to. + +Realistically, since at the time of writing JavaScript Date objects do not offer any better than millisecond precision, writing a timestamp with a microsecond directive will be followed by three zeroes, and parsing a timestamp with a microseconds `%Q` directive will erase the trailing three significant digits. + +This numeric directive can be modified using the ordinal `:` modifier. However, since it is not padded, the no-padding `-` and space-padding `_` modifiers are not applicable to this directive. + +### 12-hour time %r + +This directive is rewritten as `%I:%M:%S %p`. + +### 24-hour time %R + +This directive is rewritten as `%H:%M`. + +### Seconds since epoch %s + +The number of seconds that have elapsed since [00:00:00 on 1 January 1970](https://en.wikipedia.org/wiki/Unix_time) in the timezone that the timestamp applies to. + +This numeric directive can be modified using the ordinal `:` modifier. However, since it is not padded, the no-padding `-` and space-padding `_` modifiers are not applicable to this directive. + +### Second %S + +Second number. The number is padded with spaces if it is fewer than two digits long. The second number is normally in the range 00 through 59 and always in the range 00 through 61. + +The second number may be 60 in the case of a [leap second](https://en.wikipedia.org/wiki/Leap_second), which occur every few years. The posix standard originally allowed for the possibility of [double leap seconds](https://www.ucolick.org/~sla/leapsecs/onlinebib.html), and this is why a value of 61 is also accepted. However, there have never been two successive leap seconds in the past and this is unlikely to occur in the future. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Tab character "\t" %t + +This directive literally writes the horizontal tab character `\t` when formatting a date with **strftime** and matches the same tab character when parsing a timestamp using **strptime**. + +### 24-hour time %T + +This directive is rewritten as `%H:%M:%S`. + +### VMS date %v + +This directive is rewritten as `%e-%b-%Y`. + +### ISO 8601 week number %V + +ISO 8601 week number. Normally, this directive should be used in combination with the ISO week year directive `%G` and the weekday number directive `%u`. This number is always in the range 01 through 53. It is padded with zeroes if it is fewer than two digits long. + +You can refer to the [ISO week date article on Wikipedia](https://en.wikipedia.org/wiki/ISO_week_date) for more information about ISO 8601 week dates. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Weekday number %u + +The weekday number, starting with Monday and ending with Sunday. The number is always in the range 1 through 7. + +Note that you will get the same results parsing a weekday number with the `%u` directive regardless of whether the timestamp was written with the `%u` or the `%w` weekday number directive. + +This numeric directive can be modified using the ordinal `:` modifier. However, since it is not padded, the no-padding `-` and space-padding `_` modifiers are not applicable to this directive. + +### Week number %U + +The week number, starting from the first Sunday in the year. Days of the year that come before the first Sunday are considered to be in week zero. The number is padded with zeroes if it is fewer than two digits long. The week number is always in the range 00 through 53. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Weekday number %w + +The weekday number, starting with Sunday and ending with Saturday. The number is always in the range 0 through 6. + +Note that you will get the same results parsing a weekday number with the `%w` directive regardless of whether the timestamp was written with the `%u` or the `%w` weekday number directive. + +This numeric directive can be modified using the ordinal `:` modifier. However, since it is not padded, the no-padding `-` and space-padding `_` modifiers are not applicable to this directive. + +### Week number %W + +The week number, starting from the first Monday in the year. Days of the year that come before the first Monday are considered to be in week zero. The number is padded with zeroes if it is fewer than two digits long. The week number is always in the range 00 through 53. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Date %x + +This directive is rewritten as `%m/%d/%y`. + +### 24-hour time %X + +This directive is rewritten as `%H:%M:%S`. + +### Two-digit year %y + +Two-digit calendar year number, e.g. `18` when the year is `2018`. The number is padded with zeroes if it is fewer than two digits long. + +When parsing a two-digit year number in a timestamp that does not also have a century number `%C`, numbers less than or equal to 68 are considered to belong to the 21st century (e.g. `2068`) whereas numbers greater than 68 are considered to belong to the 20th century (e.g. `1969`). To avoid the possibility of reading the wrong year number from a timestamp outside the range 1969-2068, it is better to always write timestamps using the full calendar year directive `%Y` instead of the two-digit year `%y`. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +### Full year number %Y + +Full calendar year number. The number is padded with zeroes if it is fewer than four digits long. Note that it's possible for the year number to be negative. + +Like other numeric directives, this one can be modified using the no-padding `-`, space-padding `_`, and ordinal `:` modifiers. + +Negative year numbers are also padded to four digits, e.g. `-0099` when the calendar year number was -99 and the number was zero-padded or ` -99` when the number was space-padded. + +The full calendar year can also be made unsigned using the `^` modifier, i.e. `%^Y`. This is intended to be used in combination with the era directive `%#`, such that a calendar year of 0 produces outputs like `1 BCE` and a calendar year of -99 produces outputs like `100 BCE`. Note that this seemingly off-by-one scheme conforms to the [ISO 8601 standard](https://en.wikipedia.org/wiki/Year_zero#ISO_8601), in which year zero represents 1 BCE. + +### Timezone offset %z + +Timezone offset in hours and minutes. The offset is represented as a sign followed by two hour digits and then two minute digits. The hour and minute digits may optionally be delimited by a colon ":". For example, different offsets might be written as `+0130` or `-02:00`. The **strptime** function also recognizes the sign "±" for offset 00:00, but **strftime** will never write this character. + +The `:` modifier indicates whether the timezone offset should be formatted with a delimiter or not. When passing a format string to **strftime**, the modified `%:z` directive would write `+01:00` where the unmodified `%z` directive would write `+0100`. + +### Timezone name or offset %Z + +Timezone name, or offset in hours and minutes. The offset is represented in the same way as with the [timezone offset `%z` directive](#user-content-timezone-offset-z). However, when parsing a timestamp, abbreviations such as `UTC`, `EEST`, or `EDT` will also be recognized. The complete list of recognized abbreviations and their associated offsets can be found in the [`timezone-names.js` source file](https://github.com/pineapplemachine/strtime-js/blob/master/src/timezone-names.js). + +Note that IANA timezone names such as `America/Los_Angeles` or `Europe/Paris` are not currently recognized, although this may change in the future. + +When writing a format with **strftime** which includes the `%Z` directive, the only timezone name that is written is "UTC" when the offset is zero. Otherwise, the offset is written in the same manner as the timezone offset `%z` directive. + +The `:` modifier indicates whether the timezone offset should be formatted with a delimiter or not. When passing a format string to **strftime**, the modified `%:Z` directive would write `+01:00` where the unmodified `%Z` directive would write `+0100`. + +### Date %+ + +This directive is rewritten as `%a %b %e %H:%M:%S %Z %Y`. + +### Era name %# + +Era name, i.e. "CE" or "BCE" with the default English language settings. This should normally be used in combination with the unsigned calendar year directive `%^Y` + +The "CE" and "BCE" strings can be changed by passing an options object to **strftime** or **strptime** which includes an `eraNames` attribute. The value of this attribute must be an array two elements, a "CE" equivalent and then a "BCE" equivalent. + +The `^` modifier can be used to write switch-cased outputs, e.g. "ce" and "bce" with the default English language settings. + +The `%#` directive, when used in a format for **strptime**, is case-insensitive. This means that, for example, the string "cE" will be correctly parsed no matter whether the parsing directive is modified or not. + +### Literal "%" character %% + +This directive literally writes the percent character `%` when formatting a date with **strftime** and matches the same percent character when parsing a timestamp using **strptime**. diff --git a/readme.md b/readme.md index 86971f3..90e7132 100644 --- a/readme.md +++ b/readme.md @@ -60,11 +60,8 @@ console.log(date.toISOString()); ### Timezone output with strftime -The **strftime** function uses the inputted Date object's local timezone offset by -default. -This behavior changes when the timestamp ends with a Zulu indicator ("Z"), -for example in `%Y-%m-%dT%H:%M%SZ`; in this case it defaults to using UTC. -You can specify which timezone should be used by passing it as an argument. Timezones are accepted as numeric offsets or as abbreviations such as `UTC` or `EDT` or `EEST`. Offsets between and including -16 and +16 are interpreted as hour offsets. Other offset values are interpreted as minute offsets. +The **strftime** function defaults to writing a UTC timestamp. +You can specify which timezone should be used by passing it as an argument. Timezones are accepted as numeric offsets or as abbreviations such as `UTC` or `EDT` or `EEST`. Offsets between and including -16 and +16 are interpreted as hour offsets. Other offset values are interpreted as minute offsets. You can also use the string `local` to use the local timezone. ``` js // Prints e.g. "2000-01-01 14:00:00 GMT+0200" @@ -79,12 +76,9 @@ console.log(strftime(new Date("2000-01-01T12:00:00Z"), "%Y-%m-%d %H:%M:%S GMT%z" ### Timezone assumption with strptime -The **strptime** function assumes that a timestamp represents a date in the local -timezone if no timezone is specified in that timestamp. -Timestamps ending with a Zulu indicator ("Z"), for example in `%Y-%m-%dT%H:%M%SZ`, -are assumed to be UTC. +The **strptime** function assumes that a timestamp represents a UTC date if no timezone is specified in that timestamp. You can specify what timezone should be assumed for timestamps which do not -contain an explicit timezone by passing it as an argument. +contain an explicit timezone by passing it as an argument. The strptime function accepts timezones arguments in the same way that strftime does. ``` js // Prints e.g. "2000-01-01T10:00:00.000Z" - due to the local offset of GMT+0200 diff --git a/src/directives.js b/src/directives.js index 61e2696..2b53f0c 100644 --- a/src/directives.js +++ b/src/directives.js @@ -29,9 +29,9 @@ function writeTimezoneOffset(offsetMinutes, modifier){ // https://www.quora.com/How-does-Tomohiko-Sakamotos-Algorithm-work/answer/Raziman-T-V?srid=u2HNX function getDayOfWeek(date){ const offsets = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]; - let year = date.getFullYear(); - let month = date.getMonth(); - let day = date.getDate(); + let year = date.getUTCFullYear(); + let month = date.getUTCMonth(); + let day = date.getUTCDate(); if(month < 2){ year--; } @@ -45,14 +45,17 @@ function getDayOfWeek(date){ // Get the day of the year as a number (1-366) function getDayOfYear(date){ - const months = monthLengths.forYear(date.getFullYear()).slice(0, date.getMonth()); - return date.getDate() + ((months.length && months.reduce((a, b) => a + b)) || 0); + const lengths = monthLengths.forYear(date.getUTCFullYear()); + const months = lengths.slice(0, date.getUTCMonth()); + return date.getUTCDate() + ( + (months.length && months.reduce((a, b) => a + b)) || 0 + ); } // Get the week of the year (starting with Sunday) (0-53) function getWeekOfYearFromSunday(date){ const dayOfYear = getDayOfYear(date); - const firstDayOfWeek = getFirstWeekdayInYear(date.getFullYear()); + const firstDayOfWeek = getFirstWeekdayInYear(date.getUTCFullYear()); return Math.floor((dayOfYear + (firstDayOfWeek || 7) - 1) / 7); } @@ -60,7 +63,7 @@ function getWeekOfYearFromSunday(date){ function getWeekOfYearFromMonday(date){ const dayOfYear = getDayOfYear(date); const dayOfWeek = getDayOfWeek(date); - const firstDayOfWeek = getFirstWeekdayInYear(date.getFullYear()); + const firstDayOfWeek = getFirstWeekdayInYear(date.getUTCFullYear()); const sundayWeek = Math.floor((dayOfYear + (firstDayOfWeek || 7) - 1) / 7); return sundayWeek - (dayOfWeek === 0 ? 1 : 0) + (firstDayOfWeek === 1 ? 1 : 0); } @@ -84,7 +87,7 @@ function getISOWeeksInYear(year){ // https://en.wikipedia.org/wiki/ISO_week_date // https://en.wikipedia.org/wiki/ISO_8601#Week_dates function getISOWeekOfYear(date){ - const year = date.getFullYear(); + const year = date.getUTCFullYear(); const dayOfYear = getDayOfYear(date); const dayOfWeek = getDayOfWeek(date); const weekNumber = Math.floor((10 + dayOfYear - (dayOfWeek || 7)) / 7); @@ -99,7 +102,7 @@ function getISOWeekOfYear(date){ // https://en.wikipedia.org/wiki/ISO_week_date function getISOWeekDateYear(date){ - const year = date.getFullYear(); + const year = date.getUTCFullYear(); const dayOfYear = getDayOfYear(date); const dayOfWeek = getDayOfWeek(date); const weekNumber = Math.floor((10 + dayOfYear - (dayOfWeek || 7)) / 7); @@ -292,7 +295,7 @@ Directive.list = [ const names = ((options && options.shortWeekdayNames) || english.shortWeekdayNames ); - return names[date.getDay() % 7]; + return names[date.getUTCDay() % 7]; }, }), // Long weekday name @@ -306,7 +309,7 @@ Directive.list = [ const names = ((options && options.longWeekdayNames) || english.longWeekdayNames ); - return names[date.getDay() % 7]; + return names[date.getUTCDay() % 7]; }, }), // Abbreviated month name @@ -320,7 +323,7 @@ Directive.list = [ const names = ((options && options.shortMonthNames) || english.shortMonthNames ); - return names[date.getMonth() % 12]; + return names[date.getUTCMonth() % 12]; }, }), // Long month name @@ -334,7 +337,7 @@ Directive.list = [ const names = ((options && options.longMonthNames) || english.longMonthNames ); - return names[date.getMonth() % 12]; + return names[date.getUTCMonth() % 12]; }, }), // Combination date and time, same as "%a %b %e %H:%M:%S %Y" @@ -351,7 +354,7 @@ Directive.list = [ this.century = number; }, write: function(date){ - return Math.floor(date.getFullYear() / 100); + return Math.floor(date.getUTCFullYear() / 100); }, }), // Two-digit day of month @@ -365,7 +368,7 @@ Directive.list = [ this.dayOfMonth = number; }, write: function(date){ - return date.getDate(); + return date.getUTCDate(); }, }), // Same as %m/%d/%y @@ -384,9 +387,9 @@ Directive.list = [ }, write: function(date, modifier){ if(!modifier){ - return leftPad(" ", 2, date.getDate()); + return leftPad(" ", 2, date.getUTCDate()); }else{ - return date.getDate(); + return date.getUTCDate(); } }, }), @@ -401,7 +404,7 @@ Directive.list = [ this.microsecond = number; }, write: function(date){ - return 1000 * date.getMilliseconds(); + return 1000 * date.getUTCMilliseconds(); }, }), // Same as %Y-%m-%d @@ -423,6 +426,7 @@ Directive.list = [ // Full ISO week year new Directive({ names: ["G"], + padLength: 4, likelyLength: 4, canBeNegative: true, store: function(number){ @@ -443,7 +447,7 @@ Directive.list = [ this.hour = number; }, write: function(date){ - return date.getHours(); + return date.getUTCHours(); }, }), // Two-digit hour (1-12) to be used in combination with %p (AM/PM) @@ -457,7 +461,7 @@ Directive.list = [ this.hour = number; }, write: function(date){ - return (date.getHours() % 12) || 12; + return (date.getUTCHours() % 12) || 12; }, }), // Day in year @@ -485,7 +489,7 @@ Directive.list = [ this.millisecond = number; }, write: function(date){ - return date.getMilliseconds(); + return date.getUTCMilliseconds(); }, }), // Two-digit month number (1-12) @@ -499,7 +503,7 @@ Directive.list = [ this.month = number; }, write: function(date){ - return 1 + date.getMonth(); + return 1 + date.getUTCMonth(); }, }), // Two-digit minute (0-59) @@ -513,7 +517,7 @@ Directive.list = [ this.minute = number; }, write: function(date){ - return date.getMinutes(); + return date.getUTCMinutes(); }, }), // AM or PM (uppercase) @@ -524,7 +528,7 @@ Directive.list = [ this.meridiem = this.parseMeridiemName(); }, write: function(date, modifier, options){ - const index = date.getHours() < 12 ? 0 : 1; + const index = date.getUTCHours() < 12 ? 0 : 1; return ( (options && options.meridiemNames) || english.meridiemNames )[index]; @@ -539,7 +543,7 @@ Directive.list = [ this.meridiem = this.parseMeridiemName(); }, write: function(date, modifier, options){ - const index = date.getHours() < 12 ? 0 : 1; + const index = date.getUTCHours() < 12 ? 0 : 1; return ( (options && options.meridiemNames) || english.meridiemNames )[index].toLowerCase(); @@ -553,9 +557,7 @@ Directive.list = [ this.microsecondsSinceEpoch = number; }, write: function(date){ - // getTime is relative to UTC; result needs to be local - const time = date.getTime() - 60000 * date.getTimezoneOffset(); - return Math.floor(time * 1000); + return Math.floor(date.getTime() * 1000); }, }), // Same as "%I:%M:%S %p" @@ -576,9 +578,7 @@ Directive.list = [ this.secondsSinceEpoch = number; }, write: function(date){ - // getTime is relative to UTC; result needs to be local - const time = date.getTime() - 60000 * date.getTimezoneOffset(); - return Math.floor(time / 1000); + return Math.floor(date.getTime() / 1000); }, }), // Two-digit second (0-61) @@ -592,7 +592,7 @@ Directive.list = [ this.second = number; }, write: function(date){ - return Math.min(59, date.getSeconds()); + return Math.min(59, date.getUTCSeconds()); }, }), // Same as %H:%M:%S @@ -682,7 +682,7 @@ Directive.list = [ this.twoDigitYear = number; }, write: function(date){ - return date.getFullYear() % 100; + return date.getUTCFullYear() % 100; }, }), // Full year (usually four-digit, but not strictly so) @@ -695,7 +695,7 @@ Directive.list = [ this.year = number; }, write: function(date, modifier){ - const year = date.getFullYear(); + const year = date.getUTCFullYear(); // Modifier "^" produces unsigned year, for combination with era "%#" if(year <= 0 && modifier === "^") return 1 - year; else return year; @@ -750,7 +750,7 @@ Directive.list = [ this.era = this.parseEraName(); }, write: function(date, modifier, options){ - const index = date.getFullYear() <= 0 ? 1 : 0; + const index = date.getUTCFullYear() <= 0 ? 1 : 0; return ( (options && options.eraNames) || english.eraNames )[index]; diff --git a/src/format-time.js b/src/format-time.js index 11ce8ef..5bb1f04 100644 --- a/src/format-time.js +++ b/src/format-time.js @@ -33,18 +33,23 @@ function getFormatOptions(timezone, options){ function getTimezoneOffsetMinutes(date, tz){ if(tz === null || tz === undefined){ - return undefined; + return 0; }else if(tz >= -16 && tz <= +16){ return Math.floor(60 * tz); }else if(Number.isFinite(tz)){ return Math.floor(tz); }else if(tz === "local"){ - return -(date || new Date()).getTimezoneOffset() - }else if(tz in defaultTimezoneNames){ - return Math.floor(60 * defaultTimezoneNames[tz]); + return -(date || new Date()).getTimezoneOffset(); }else{ - throw new Error(`Unrecognized timezone option "${tz}".`); + const tzUpper = String(tz).toUpperCase(); + if(tzUpper in defaultTimezoneNames){ + const offset = Math.floor(60 * defaultTimezoneNames[tzUpper]); + if(Number.isFinite(offset)){ + return offset; + } + } } + throw new Error(`Unrecognized timezone option "${tz}".`); } function strftime(date, format, timezone, options){ @@ -71,14 +76,8 @@ function strftime(date, format, timezone, options){ if(timezoneOffsetMinutes !== undefined){ tzDate.setUTCMinutes( date.getUTCMinutes() + - date.getTimezoneOffset() + timezoneOffsetMinutes ); - }else if(tokens.zuluTimezone){ - tzDate.setUTCMinutes( - date.getUTCMinutes() + - date.getTimezoneOffset() - ); } let output = ""; for(let token of tokens){ @@ -99,8 +98,6 @@ function strptime(timestamp, format, timezone, options){ const timezoneOffsetMinutes = getTimezoneOffsetMinutes(undefined, useOptions.tz); if(timezoneOffsetMinutes !== undefined){ parser.timezoneOffsetMinutes = timezoneOffsetMinutes; - }else if(parser.tokens.zuluTimezone){ - parser.timezoneOffsetMinutes = 0; } if(useOptions.options){ for(let key in useOptions.options){ diff --git a/test/canary-test.js b/test/canary-test.js index 41220a4..a6129dc 100644 --- a/test/canary-test.js +++ b/test/canary-test.js @@ -43,6 +43,7 @@ function createTests(strtime){ assert.equal(strftime(getUTCDate({year: 2018, month: 5, day: 17}), "%a"), "Thu"); assert.equal(strftime(getUTCDate({year: 2018, month: 5, day: 18}), "%a"), "Fri"); assert.equal(strftime(getUTCDate({year: 2018, month: 5, day: 19}), "%a"), "Sat"); + assert.equal(strftime(getUTCDate({year: 2018, month: 5, day: 19}), "%^a"), "SAT"); }); this.test("parse", function(){ assert.deepStrictEqual(strptime("2018-W20-Fri", "%G-W%V-%a", {tz: 0}), getUTCDate({year: 2018, month: 5, day: 18})); @@ -133,7 +134,7 @@ function createTests(strtime){ this.group("Century number %C", function(){ this.test("format", function(){ assert.equal(strftime(new Date("2000-06-15"), "%C"), "20"); - assert.equal(strftime(getDate({year: -2000}), "%C"), "-20"); + assert.equal(strftime(getUTCDate({year: -2000}), "%C"), "-20"); }); this.test("parse", function(){ assert.equal(strptime("20", "%C").getFullYear(), 2000); @@ -211,7 +212,11 @@ function createTests(strtime){ this.group("Full ISO week year %G", function(){ this.test("format", function(){ assert.equal(strftime(new Date("2000-06-15"), "%G"), "2000"); - assert.equal(strftime(getDate({year: -2000, month: 6}), "%G"), "-2000"); + assert.equal(strftime(getUTCDate({year: 200, month: 6}), "%G"), "0200"); + assert.equal(strftime(getUTCDate({year: -2000, month: 6}), "%G"), "-2000"); + assert.equal(strftime(getUTCDate({year: -0029, month: 6}), "%G"), "-0029"); + assert.equal(strftime(getUTCDate({year: -0029, month: 6}), "%_G"), " -29"); + assert.equal(strftime(getUTCDate({year: -0029, month: 6}), "%-G"), "-29"); }); this.test("parse", function(){ assert.equal(strptime("2000-W10", "%G-W%V").getFullYear(), 2000); @@ -498,7 +503,11 @@ function createTests(strtime){ this.group("Full year %Y", function(){ this.test("format", function(){ assert.equal(strftime(new Date("2000-06-15"), "%Y"), "2000"); - assert.equal(strftime(getDate({year: -2000}), "%Y"), "-2000"); + assert.equal(strftime(getUTCDate({year: -2000}), "%Y"), "-2000"); + assert.equal(strftime(getUTCDate({year: -59}), "%Y"), "-0059"); + assert.equal(strftime(getUTCDate({year: -59}), "%_Y"), " -59"); + assert.equal(strftime(getUTCDate({year: -59}), "%-Y"), "-59"); + assert.equal(strftime(getUTCDate({year: -59}), "%^Y"), "60"); }); this.test("parse", function(){ assert.equal(strptime("2000", "%Y").getFullYear(), 2000); @@ -544,7 +553,9 @@ function createTests(strtime){ assert.equal(strptime("12:00 +01:00", "%H:%M %Z").getUTCMinutes(), 0); assert.equal(strptime("12:00 Z", "%H:%M %Z").getUTCHours(), 12); assert.equal(strptime("12:00 UTC", "%H:%M %Z").getUTCHours(), 12); + assert.equal(strptime("12:00 utc", "%H:%M %Z").getUTCHours(), 12); assert.equal(strptime("12:00 EDT", "%H:%M %Z").getUTCHours(), 16); // -4 + assert.equal(strptime("12:00 edt", "%H:%M %Z").getUTCHours(), 16); // -4 assert.equal(strptime("12:00 EEST", "%H:%M %Z").getUTCHours(), 9); // +3 assert.equal(strptime("12:00 ACDT", "%H:%M %Z").getUTCHours(), 1); // +10.5 assert.equal(strptime("12:00 ACDT", "%H:%M %Z").getUTCMinutes(), 30); @@ -601,11 +612,7 @@ function createTests(strtime){ }); canary.group("common timestamp formats", function(){ - const date = getDate({ - year: 2018, month: 5, day: 4, - hour: 22, minute: 15, second: 30, - millisecond: 0, - }); + const date = new Date("2018-05-04T22:15:30.000Z"); this.test("write common formats", function(){ assert.equal(strftime(date, "%F %T"), "2018-05-04 22:15:30"); assert.equal(strftime(date, "%Y-%m-%d %H:%M:%S"), "2018-05-04 22:15:30"); @@ -625,7 +632,7 @@ function createTests(strtime){ }); canary.group("potentially ambiguous number formats", function(){ - const date = getDate({year: 2018, month: 5, day: 4}); + const date = new Date("2018-05-04"); this.test("write potentially ambiguous formats", function(){ assert.equal(strftime(date, "%Y%m%d"), "20180504"); assert.equal(strftime(date, "%Y%-m%-d"), "201854"); @@ -714,7 +721,10 @@ function createTests(strtime){ canary.group("providing an explicit timezone option", function(){ const date = new Date("2018-06-15T12:30:00Z"); - this.test("write using the default timezone (local)", function(){ + this.test("write using the default timezone (UTC)", function(){ + assert.equal(strftime(date, "%F %T %z"), "2018-06-15 12:30:00 +0000"); + }); + this.test("write using the local timezone", function(){ const localTimezoneOffset = -(date.getTimezoneOffset()); const absOffset = Math.abs(localTimezoneOffset); const tzSign = localTimezoneOffset >= 0 ? "+" : "-"; @@ -724,11 +734,12 @@ function createTests(strtime){ (tzHours >= 10 ? tzHours : `0${tzHours}`) + (tzMinutes >= 10 ? tzMinutes : `0${tzMinutes}`) ); - assert.equal(strftime(date, "%z"), tzString); // e.g. "+0200" + assert.equal(strftime(date, "%z", "local"), tzString); // e.g. "+0200" }); this.test("write with a specific timezone", function(){ assert.equal(strftime(date, "%F %T %z", {tz: 0}), "2018-06-15 12:30:00 +0000"); assert.equal(strftime(date, "%F %T %z", {tz: "UTC"}), "2018-06-15 12:30:00 +0000"); + assert.equal(strftime(date, "%F %T %z", {tz: "utc"}), "2018-06-15 12:30:00 +0000"); assert.equal(strftime(date, "%F %T %z", {tz: +300}), "2018-06-15 17:30:00 +0500"); assert.equal(strftime(date, "%F %T %z", {tz: -300}), "2018-06-15 07:30:00 -0500"); assert.equal(strftime(date, "%F %T %z", "UTC"), "2018-06-15 12:30:00 +0000"); @@ -740,7 +751,7 @@ function createTests(strtime){ assert.equal(strftime(date, "%F %T %z", +120), "2018-06-15 14:30:00 +0200"); assert.equal(strftime(date, "%F %T %z", -120), "2018-06-15 10:30:00 -0200"); }); - this.test("write with explicit timezone overrides 'Z' ending", function(){ + this.test("write with explicit timezone ignores 'Z' (Zulu) ending", function(){ assert.equal(strftime(new Date("2018-01-01"), "%FT%TZ"), "2018-01-01T00:00:00Z"); assert.equal(strftime(new Date("2018-01-01"), "%FT%TZ", {tz: +2}), "2018-01-01T02:00:00Z"); }); @@ -752,10 +763,14 @@ function createTests(strtime){ return true; }); }); + this.test("parse assuming the default timezone (UTC)", function(){ + assert.deepStrictEqual(strptime("2018-06-15 12:30:00", "%F %T"), date); + }); this.test("parse assuming a certain timezone when none is in the timestamp", function(){ const ts = "2018-08-15 12:30:00"; assert.deepStrictEqual(strptime(ts, "%F %T", {tz: 0}), new Date("2018-08-15T12:30:00Z")); assert.deepStrictEqual(strptime(ts, "%F %T", {tz: "UTC"}), new Date("2018-08-15T12:30:00Z")); + assert.deepStrictEqual(strptime(ts, "%F %T", {tz: "utc"}), new Date("2018-08-15T12:30:00Z")); assert.deepStrictEqual(strptime(ts, "%F %T", {tz: +60}), new Date("2018-08-15T11:30:00Z")); assert.deepStrictEqual(strptime(ts, "%F %T", {tz: -60}), new Date("2018-08-15T13:30:00Z")); assert.deepStrictEqual(strptime(ts, "%F %T", {tz: +2.5}), new Date("2018-08-15T10:00:00Z")); @@ -763,7 +778,7 @@ function createTests(strtime){ assert.deepStrictEqual(strptime(ts, "%F %T", 0), new Date("2018-08-15T12:30:00Z")); assert.deepStrictEqual(strptime(ts, "%F %T", "UTC"), new Date("2018-08-15T12:30:00Z")); }); - this.test("parse with explicit timezone overrides 'Z' ending", function(){ + this.test("parse with explicit timezone ignores 'Z' (Zulu) ending", function(){ assert(strptime("2018-01-01T04:00:00Z", "%FT%TZ").getUTCHours() === 4); assert(strptime("2018-01-01T04:00:00Z", "%FT%TZ", {tz: +2}).getUTCHours() === 2); }); @@ -934,14 +949,14 @@ function createTests(strtime){ }); this.test("strftime accepts dayjs inputs", function(){ const date = dayjs('2018-08-08'); - assert.equal(strftime(date, "%F %T"), "2018-08-08 00:00:00"); + assert.equal(strftime(date, "%F %T", "local"), "2018-08-08 00:00:00"); }); this.test("strftime accepts luxon inputs", function(){ - const date = luxon.DateTime.local(2017, 5, 15); + const date = luxon.DateTime.utc(2017, 5, 15); assert.equal(strftime(date, "%F %T"), "2017-05-15 00:00:00"); }); this.test("strftime accepts moment inputs", function(){ - const date = moment('1995-12-25'); + const date = moment.utc('1995-12-25'); assert.equal(strftime(date, "%F %T"), "1995-12-25 00:00:00"); }); this.test("strftime throws an error for null and undefined inputs", function(){