Nothing
#' Format a datetime with a formatting string
#'
#' With `fdt()`, we can format datetime values with the greatest of ease, and,
#' with great power. There is a lot of leniency in what types of input
#' date/time/datetime values can be passed in. The formatting string allows for
#' a huge array of possibilities when formatting. Not only that, we can set a
#' `locale` value and get the formatted values localized in the language/region
#' of choice. There's plenty of ways to represent time zone information, and
#' this goes along with the option to enrich the input values with a precise
#' time zone identifier (like `"America/Los_Angeles"`). The choices are ample
#' here, with the goal being a comprehensiveness and ease-of-use in date/time
#' formatting.
#'
#' @section Valid Input Values:
#'
#' The `input` argument of the `fdt()` function allows for some flexibility on
#' what can be passed in. This section describes the kinds of inputs that are
#' understandable by `fdt()`. A vector of strings is allowed, as are vectors of
#' `Date` or `POSIXct` values.
#'
#' If using strings, a good option is to use those that adhere to the ISO
#' 8601:2004 standard. For a datetime this can be of the form
#' `YYYY-MM-DDThh:mm:ss.s<TZD>`. With this, `YYYY-MM-DD` corresponds to the
#' date, the literal `"T"` is optional, `hh:mm:ss` is the time (where seconds,
#' `ss`, is optional as are `.s` for fractional seconds), and `<TZD>` refers to
#' an optional time-zone designation (more on time zones in the next paragraph).
#' You can provide just the date part, and this assumes midnight as an implicit
#' time. It's also possible to provide just the time part, and this internally
#' assembles a datetime that uses the current date. When formatting standalone
#' dates or times, you'll probably just format the explicit parts but `fdt()`
#' won't error if you format the complementary parts.
#'
#' The time zone designation on string-based datetimes is completely optional.
#' If not provided then `"UTC"` is assumed. If you do want to supply time zone
#' information, it can be given as an offset value with the following
#' constructions:
#'
#' - `<time>Z`
#' - `<time>(+/-)hh:mm`
#' - `<time>(+/-)hhmm`
#' - `<time>(+/-)hh`
#'
#' The first, `<time>Z`, is zone designator for the zero UTC offset; it's
#' equivalent to `"+00:00"`. The next two are formats for providing the time
#' offsets from UTC with hours and minutes fields. Examples are `"-05:00"` (New
#' York, standard time), `"+0200"` (Cairo), and `"+05:30"` (Mumbai). Note that
#' the colon is optional but leading zeros to maintain two-digit widths are
#' essential. The final format, `<time>(+/-)hh`, omits the minutes field and as
#' so many offsets have `"00"` minutes values, this can be convenient.
#'
#' We can also supply an Olson/IANA-style time zone identifier (tzid) in
#' parentheses within the string, or, as a value supplied to `use_tz` (should a
#' tzid apply to all date/time/datetime values in the `input` vector). By
#' extension, this would use the form: `YYYY-MM-DDThh:mm:ss.s<TZD>(<tzid>)`.
#' Both a `<TZD>` (UTC offset value) and a `<tzid>` shouldn't really be used
#' together but if that occurs the `<tzid>` overrides the UTC offset. Here are
#' some examples:
#'
#' - `"2018-07-04T22:05 (America/Vancouver)"` (preferable)
#' - `"2018-07-04T22:05-0800(America/Vancouver)"` (redundant, but still okay)
#'
#' A tzid contains much more information about the time zone than a UTC offset
#' value since it is tied to some geographical location and the timing of
#' Standard Time (STD) and Daylight Saving Time (DST) is known. In essence we
#' can derive UTC offset values from a tzid and also a host of other identifiers
#' (time zone names, their abbreviations, etc.). Here are some examples of valid
#' tzid values that can be used:
#'
#' - `"America/Jamaica"` (the official time in Jamaica, or, `"Jamaica Time"`)
#' - `"Australia/Perth"` (`"+08:00"` year round in Western Australia)
#' - `"Europe/Dublin"` (IST/GMT time: `"+01:00"`/`"+00:00"`)
#'
#' The tz database (a compilation of information about the world's time zones)
#' consists of canonical zone names (those that are primary and preferred) and
#' alternative names (less preferred in modern usage, and was either discarded
#' or more commonly replaced by a canonical zone name). The `fdt()` function can
#' handle both types and what occurs is that non-canonical tzid values are
#' internally mapped onto canonical zone names. Here's a few examples:
#'
#' - `"Africa/Luanda"` (in Angola) maps to `"Africa/Lagos"`
#' - `"America/Indianapolis"` maps to `"America/Indiana/Indianapolis"`
#' - `"Asia/Calcutta"` maps to `"Asia/Kolkata"`
#' - `"Pacific/Midway"` maps to `"Pacific/Pago_Pago"`
#' - `"Egypt"` maps to `"Africa/Cairo"`
#'
#' For the most part, the Olson-format tzid follows the form `"{region}/{city}"`
#' where the region is usually a continent, the city is considered an 'exemplar
#' city', and the exemplar city itself belongs in a country.
#'
#' @section Date/Time Format Syntax:
#'
#' A formatting pattern as used in **bigD** consists of a string of characters,
#' where certain strings are replaced with date and time data that are derived
#' from the parsed input.
#'
#' The characters used in patterns are tabulated below to show which specific
#' strings produce which outputs (e.g., `"y"` for the year). A common pattern is
#' characters that are used consecutively to produce variations on a date, time,
#' or timezone output. Say that the year in the input is 2015. If using `"yy"`
#' you'll get `"15"` but with `"yyyy"` the output becomes `"2015"`. There's a
#' whole lot of this, so the following subsections try to illustrate as best as
#' possible what each string will produce. All of the examples will use this
#' string-based datetime input unless otherwise indicated:
#'
#' `"2018-07-04T22:05:09.2358(America/Vancouver)"`
#'
#' ## Year
#'
#' ### Calendar Year (little y)
#'
#' This yields the calendar year, which is always numeric. In most cases the
#' length of the `"y"` field specifies the minimum number of digits to display,
#' zero-padded as necessary. More digits will be displayed if needed to show the
#' full year. There is an exception: `"yy"` gives use just the two low-order
#' digits of the year, zero-padded as necessary. For most use cases, `"y"` or
#' `"yy"` should be good enough.
#'
#' | Field Patterns | Output |
#' |------------------------------- |----------------------------------------|
#' | `"y"` | `"2018"` |
#' | `"yy"` | `"18"` |
#' | `"yyy"` to `"yyyyyyyyy"` | `"2018"` to `"000002018"` |
#'
#' ### Year in the Week in Year Calendar (big Y)
#'
#' This is the year in 'Week of Year' based calendars in which the year
#' transition occurs on a week boundary. This may differ from calendar year
#' `"y"` near a year transition. This numeric year designation is used in
#' conjunction with pattern character `"w"` in the ISO year-week calendar as
#' defined by ISO 8601.
#'
#' | Field Patterns | Output |
#' |--------------------------------|----------------------------------------|
#' | `"Y"` | `"2018"` |
#' | `"YY"` | `"18"` |
#' | `"YYY"` to `"YYYYYYYYY"` | `"2018"` to `"000002018"` |
#'
#' ## Quarter
#'
#' ### Quarter of the Year: formatting (big Q) and standalone (little q)
#'
#' The quarter names are identified numerically, starting at `1` and ending at
#' `4`. Quarter names may vary along two axes: the width and the context. The
#' context is either 'formatting' (taken as a default), which the form used
#' within a complete date format string, or, 'standalone', the form for date
#' elements used independently (such as in calendar headers). The standalone
#' form may be used in any other date format that shares the same form of the
#' name. Here, the formatting form for quarters of the year consists of some run
#' of `"Q"` values whereas the standalone form uses `"q"`.
#'
#' | Field Patterns | Output | Notes |
#' |-------------------|-----------------|-----------------------------------|
#' | `"Q"`/`"q"` | `"3"` | Numeric, one digit |
#' | `"QQ"`/`"qq"` | `"03"` | Numeric, two digits (zero padded) |
#' | `"QQQ"`/`"qqq"` | `"Q3"` | Abbreviated |
#' | `"QQQQ"`/`"qqqq"` | `"3rd quarter"` | Wide |
#' | `"QQQQQ"`/`"qqqqq"` | `"3"` | Narrow |
#'
#' ## Month
#'
#' ### Month: formatting (big M) and standalone (big L)
#'
#' The month names are identified numerically, starting at `1` and ending at
#' `12`. Month names may vary along two axes: the width and the context. The
#' context is either 'formatting' (taken as a default), which the form used
#' within a complete date format string, or, 'standalone', the form for date
#' elements used independently (such as in calendar headers). The standalone
#' form may be used in any other date format that shares the same form of the
#' name. Here, the formatting form for months consists of some run of `"M"`
#' values whereas the standalone form uses `"L"`.
#'
#' | Field Patterns | Output | Notes |
#' |-------------------|-----------------|-----------------------------------|
#' | `"M"`/`"L"` | `"7"` | Numeric, minimum digits |
#' | `"MM"`/`"LL"` | `"07"` | Numeric, two digits (zero padded) |
#' | `"MMM"`/`"LLL"` | `"Jul"` | Abbreviated |
#' | `"MMMM"`/`"LLLL"` | `"July"` | Wide |
#' | `"MMMMM"`/`"LLLLL"` | `"J"` | Narrow |
#'
#' ## Week
#'
#' ### Week of Year (little w)
#'
#' Values calculated for the week of year range from `1` to `53`. Week `1` for a
#' year is the first week that contains at least the specified minimum number of
#' days from that year. Weeks between week `1` of one year and week `1` of the
#' following year are numbered sequentially from `2` to `52` or `53` (if
#' needed).
#'
#' There are two available field lengths. Both will display the week of year
#' value but the `"ww"` width will always show two digits (where weeks `1` to
#' `9` are zero padded).
#'
#' | Field Patterns | Output | Notes |
#' |------------------|-----------|------------------------------------------|
#' | `"w"` | `"27"` | Minimum digits |
#' | `"ww"` | `"27"` | Two digits (zero padded) |
#'
#' ### Week of Month (big W)
#'
#' The week of a month can range from `1` to `5`. The first day of every month
#' always begins at week `1` and with every transition into the beginning of a
#' week, the week of month value is incremented by `1`.
#'
#' | Field Pattern | Output |
#' |------------------|------------------------------------------------------|
#' | `"W"` | `"1"` |
#'
#' ## Day
#'
#' ### Day of Month (little d)
#'
#' The day of month value is always numeric and there are two available field
#' length choices in its formatting. Both will display the day of month value
#' but the `"dd"` formatting will always show two digits (where days `1` to `9`
#' are zero padded).
#'
#' | Field Patterns | Output | Notes |
#' |----------------|-----------|--------------------------------------------|
#' | `"d"` | `"4"` | Minimum digits |
#' | `"dd"` | `"04"` | Two digits, zero padded |
#'
#' ### Day of Year (big D)
#'
#' The day of year value ranges from `1` (January 1) to either `365` or `366`
#' (December 31), where the higher value of the range indicates that the year is
#' a leap year (29 days in February, instead of 28). The field length specifies
#' the minimum number of digits, with zero-padding as necessary.
#'
#' | Field Patterns | Output | Notes |
#' |-----------------|----------|--------------------------------------------|
#' | `"D"` | `"185"` | |
#' | `"DD"` | `"185"` | Zero padded to minimum width of 2 |
#' | `"DDD"` | `"185"` | Zero padded to minimum width of 3 |
#'
#' ### Day of Week in Month (big F)
#'
#' The day of week in month returns a numerical value indicating the number of
#' times a given weekday had occurred in the month (e.g., '2nd Monday in
#' March'). This conveniently resolves to predicable case structure where ranges
#' of day of the month values return predictable day of week in month values:
#'
#' - days `1` - `7` -> `1`
#' - days `8` - `14` -> `2`
#' - days `15` - `21` -> `3`
#' - days `22` - `28` -> `4`
#' - days `29` - `31` -> `5`
#'
#' | Field Pattern | Output |
#' |--------------------------------|----------------------------------------|
#' | `"F"` | `"1"` |
#'
#' ### Modified Julian Date (little g)
#'
#' The modified version of the Julian date is obtained by subtracting
#' 2,400,000.5 days from the Julian date (the number of days since January 1,
#' 4713 BC). This essentially results in the number of days since midnight
#' November 17, 1858. There is a half day offset (unlike the Julian date, the
#' modified Julian date is referenced to midnight instead of noon).
#'
#' | Field Patterns | Output |
#' |--------------------------------|----------------------------------------|
#' | `"g"` to `"ggggggggg"` | `"58303"` -> `"000058303"` |
#'
#' ## Weekday
#'
#' ### Day of Week Name (big E)
#'
#' The name of the day of week is offered in four different widths.
#'
#' | Field Patterns | Output | Notes |
#' |----------------------------|----------------|---------------------------|
#' | `"E"`, `"EE"`, or `"EEE"` | `"Wed"` | Abbreviated |
#' | `"EEEE"` | `"Wednesday"` | Wide |
#' | `"EEEEE"` | `"W"` | Narrow |
#' | `"EEEEEE"` | `"We"` | Short |
#'
#' ## Periods
#'
#' ### AM/PM Period of Day (little a)
#'
#' This denotes before noon and after noon time periods. May be upper or
#' lowercase depending on the locale and other options. The wide form may be
#' the same as the short form if the 'real' long form (e.g. 'ante meridiem') is
#' not customarily used. The narrow form must be unique, unlike some other
#' fields.
#'
#' | Field Patterns | Output | Notes |
#' |--------------------------------|----------|-----------------------------|
#' | `"a"`, `"aa"`, or `"aaa"` | `"PM"` | Abbreviated |
#' | `"aaaa"` | `"PM"` | Wide |
#' | `"aaaaa"` | `"p"` | Narrow |
#'
#' ### AM/PM Period of Day Plus Noon and Midnight (little b)
#'
#' Provide AM and PM as well as phrases for exactly noon and midnight. May be
#' upper or lowercase depending on the locale and other options. If the locale
#' doesn't have the notion of a unique 'noon' (i.e., 12:00), then the PM form
#' may be substituted. A similar behavior can occur for 'midnight' (00:00) and
#' the AM form. The narrow form must be unique, unlike some other fields.
#'
#' (a) `input_midnight`: `"2020-05-05T00:00:00"`
#' (b) `input_noon`: `"2020-05-05T12:00:00"`
#'
#' | Field Patterns | Output | Notes |
#' |--------------------------------|--------------------|-------------------|
#' | `"b"`, `"bb"`, or `"bbb"` | (a) `"midnight"` | Abbreviated |
#' | | (b) `"noon"` | |
#' | `"bbbb"` | (a) `"midnight"` | Wide |
#' | | (b) `"noon"` | |
#' | `"bbbbb"` | (a) `"mi"` | Narrow |
#' | | (b) `"n"` | |
#'
#' ### Flexible Day Periods (big B)
#'
#' Flexible day periods denotes things like 'in the afternoon', 'in the
#' evening', etc., and the flexibility comes from a locale's language and
#' script. Each locale has an associated rule set that specifies when the day
#' periods start and end for that locale.
#'
#' (a) `input_morning`: `"2020-05-05T00:08:30"`
#' (b) `input_afternoon`: `"2020-05-05T14:00:00"`
#'
#' | Field Patterns | Output | Notes |
#' |----------------------------|--------------------------|-----------------|
#' | `"B"`, `"BB"`, or `"BBB"` | (a) `"in the morning"` | Abbreviated |
#' | | (b) `"in the afternoon"` | |
#' | `"BBBB"` | (a) `"in the morning"` | Wide |
#' | | (b) `"in the afternoon"` | |
#' | `"BBBBB"` | (a) `"in the morning"` | Narrow |
#' | | (b) `"in the afternoon"` | |
#'
#' ## Hours, Minutes, and Seconds
#'
#' ### Hour 0-23 (big H)
#'
#' Hours from `0` to `23` are for a standard 24-hour clock cycle (midnight plus
#' 1 minute is `00:01`) when using `"HH"` (which is the more common width that
#' indicates zero-padding to 2 digits).
#'
#' Using `"2015-08-01T08:35:09"`:
#'
#' | Field Patterns | Output | Notes |
#' |------------------------|---------|--------------------------------------|
#' | `"H"` | `"8"` | Numeric, minimum digits |
#' | `"HH"` | `"08"` | Numeric, 2 digits (zero padded) |
#'
#' ### Hour 1-12 (little h)
#'
#' Hours from `1` to `12` are for a standard 12-hour clock cycle (midnight plus
#' 1 minute is `12:01`) when using `"hh"` (which is the more common width that
#' indicates zero-padding to 2 digits).
#'
#' Using `"2015-08-01T08:35:09"`:
#'
#' | Field Patterns | Output | Notes |
#' |------------------------|---------|--------------------------------------|
#' | `"h"` | `"8"` | Numeric, minimum digits |
#' | `"hh"` | `"08"` | Numeric, 2 digits (zero padded) |
#'
#' ### Hour 1-24 (little k)
#'
#' Using hours from `1` to `24` is a less common way to express a 24-hour clock
#' cycle (midnight plus 1 minute is `24:01`) when using `"kk"` (which is the
#' more common width that indicates zero-padding to 2 digits).
#'
#' Using `"2015-08-01T08:35:09"`:
#'
#' | Field Patterns | Output | Notes |
#' |------------------------|---------|--------------------------------------|
#' | `"k"` | `"9"` | Numeric, minimum digits |
#' | `"kk"` | `"09"` | Numeric, 2 digits (zero padded) |
#'
#' ### Hour 0-11 (big K)
#'
#' Using hours from `0` to `11` is a less common way to express a 12-hour clock
#' cycle (midnight plus 1 minute is `00:01`) when using `"KK"` (which is the
#' more common width that indicates zero-padding to 2 digits).
#'
#' Using `"2015-08-01T08:35:09"`:
#'
#' | Field Patterns | Output | Notes |
#' |------------------------|---------|--------------------------------------|
#' | `"K"` | `"7"` | Numeric, minimum digits |
#' | `"KK"` | `"07"` | Numeric, 2 digits (zero padded) |
#'
#' ### Minute (little m)
#'
#' The minute of the hour which can be any number from `0` to `59`. Use `"m"` to
#' show the minimum number of digits, or `"mm"` to always show two digits
#' (zero-padding, if necessary).
#'
#' | Field Patterns | Output | Notes |
#' |------------------------|---------|--------------------------------------|
#' | `"m"` | `"5"` | Numeric, minimum digits |
#' | `"mm"` | `"06"` | Numeric, 2 digits (zero padded) |
#'
#' ### Seconds (little s)
#'
#' The second of the minute which can be any number from `0` to `59`. Use `"s"`
#' to show the minimum number of digits, or `"ss"` to always show two digits
#' (zero-padding, if necessary).
#'
#' | Field Patterns | Output | Notes |
#' |------------------------|---------|--------------------------------------|
#' | `"s"` | `"9"` | Numeric, minimum digits |
#' | `"ss"` | `"09"` | Numeric, 2 digits (zero padded) |
#'
#' ### Fractional Second (big S)
#'
#' The fractional second truncates (like other time fields) to the width
#' requested (i.e., count of letters). So using pattern `"SSSS"` will display
#' four digits past the decimal (which, incidentally, needs to be added manually
#' to the pattern).
#'
#' | Field Patterns | Output |
#' |--------------------------------|----------------------------------------|
#' | `"S"` to `"SSSSSSSSS"` | `"2"` -> `"235000000"` |
#'
#' ### Milliseconds Elapsed in Day (big A)
#'
#' There are 86,400,000 milliseconds in a day and the `"A"` pattern will provide
#' the whole number. The width can go up to nine digits with `"AAAAAAAAA"` and
#' these higher field widths will result in zero padding if necessary.
#'
#' Using `"2011-07-27T00:07:19.7223"`:
#'
#' | Field Patterns | Output |
#' |--------------------------------|----------------------------------------|
#' | `"A"` to `"AAAAAAAAA"` | `"439722"` -> `"000439722"` |
#'
#' ## Era
#'
#' ### The Era Designator (big G)
#'
#' This provides the era name for the given date. The Gregorian calendar has two
#' eras: AD and BC. In the AD year numbering system, AD 1 is immediately
#' preceded by 1 BC, with nothing in between them (there was no year zero).
#'
#' | Field Patterns | Output | Notes |
#' |--------------------------------|-----------------|----------------------|
#' | `"G"`, `"GG"`, or `"GGG"` | `"AD"` | Abbreviated |
#' | `"GGGG"` | `"Anno Domini"` | Wide |
#' | `"GGGGG"` | `"A"` | Narrow |
#'
#' ## Time Zones
#'
#' ### TZ // Short and Long Specific non-Location Format (little z)
#'
#' The short and long specific non-location formats for time zones are suggested
#' for displaying a time with a user friendly time zone name. Where the short
#' specific format is unavailable, it will fall back to the short localized GMT
#' format (`"O"`). Where the long specific format is unavailable, it will fall
#' back to the long localized GMT format (`"OOOO"`).
#'
#' | Field Patterns | Output | Notes |
#' |----------------------------|---------------------------|----------------|
#' | `"z"`, `"zz"`, or `"zzz"` | `"PDT"` | Short Specific |
#' | `"zzzz"` | `"Pacific Daylight Time"` | Long Specific |
#'
#' ### TZ // Common UTC Offset Formats (big Z)
#'
#' The ISO8601 basic format with hours, minutes and optional seconds fields is
#' represented by `"Z"`, `"ZZ"`, or `"ZZZ"`. The format is equivalent to RFC 822
#' zone format (when the optional seconds field is absent). This is equivalent
#' to the `"xxxx"` specifier. The field pattern `"ZZZZ"` represents the long
#' localized GMT format. This is equivalent to the `"OOOO"` specifier. Finally,
#' `"ZZZZZ"` pattern yields the ISO8601 extended format with hours, minutes and
#' optional seconds fields. The ISO8601 UTC indicator `Z` is used when local
#' time offset is `0`. This is equivalent to the `"XXXXX"` specifier.
#'
#' | Field Patterns | Output | Notes |
#' |----------------------------|--------------|-----------------------------|
#' | `"Z"`, `"ZZ"`, or `"ZZZ"` | `"-0700"` | ISO 8601 basic format |
#' | `"ZZZZ"` | `"GMT-7:00"` | Long localized GMT format |
#' | `"ZZZZZ"` | `"-07:00"` | ISO 8601 extended format |
#'
#' ### TZ // Short and Long Localized GMT Formats (big O)
#'
#' The localized GMT formats come in two widths `"O"` (which removes the minutes
#' field if it's `0`) and `"OOOO"` (which always contains the minutes field).
#' The use of the `GMT` indicator changes according to the locale.
#'
#' | Field Patterns | Output | Notes |
#' |-------------------------|---------------|-------------------------------|
#' | `"O"` | `"GMT-7"` | Short localized GMT format |
#' | `"OOOO"` | `"GMT-07:00"` | Long localized GMT format |
#'
#' ### TZ // Short and Long Generic non-Location Formats (little v)
#'
#' The generic non-location formats are useful for displaying a recurring wall
#' time (e.g., events, meetings) or anywhere people do not want to be overly
#' specific. Where either of these is unavailable, there is a fallback to the
#' generic location format (`"VVVV"`), then the short localized GMT format as
#' the final fallback.
#'
#' | Field Patterns | Output | Notes |
#' |-----------------|------------------|------------------------------------|
#' | `"v"` | `"PT"` | Short generic non-location format |
#' | `"vvvv"` | `"Pacific Time"` | Long generic non-location format |
#'
#' ### TZ // Short Time Zone IDs and Exemplar City Formats (big V)
#'
#' These formats provide variations of the time zone ID and often include the
#' exemplar city. The widest of these formats, `"VVVV"`, is useful for
#' populating a choice list for time zones, because it supports 1-to-1 name/zone
#' ID mapping and is more uniform than other text formats.
#'
#' | Field Patterns | Output | Notes |
#' |--------------------|-----------------------|----------------------------|
#' | `"V"` | `"cavan"` | Short time zone ID |
#' | `"VV"` | `"America/Vancouver"` | Long time zone ID |
#' | `"VVV"` | `"Vancouver"` | The tz exemplar city |
#' | `"VVVV"` | `"Vancouver Time"` | Generic location format |
#'
#' ### TZ // ISO 8601 Formats with Z for +0000 (big X)
#'
#' The `"X"`-`"XXX"` field patterns represent valid ISO 8601 patterns for time
#' zone offsets in datetimes. The final two widths, `"XXXX"` and `"XXXXX"` allow
#' for optional seconds fields. The seconds field is *not* supported by the ISO
#' 8601 specification. For all of these, the ISO 8601 UTC indicator `Z` is used
#' when the local time offset is `0`.
#'
#' | Field Patterns | Output | Notes |
#' |----------------|------------|-------------------------------------------|
#' | `"X"` | `"-07"` | ISO 8601 basic format (h, optional m) |
#' | `"XX"` | `"-0700"` | ISO 8601 basic format (h & m) |
#' | `"XXX"` | `"-07:00"` | ISO 8601 extended format (h & m) |
#' | `"XXXX"` | `"-0700"` | ISO 8601 basic format (h & m, optional s) |
#' | `"XXXXX"` | `"-07:00"` | ISO 8601 extended format (h & m, optional s) |
#'
#' ### TZ // ISO 8601 Formats (no use of Z for +0000) (little x)
#'
#' The `"x"`-`"xxxxx"` field patterns represent valid ISO 8601 patterns for time
#' zone offsets in datetimes. They are similar to the `"X"`-`"XXXXX"` field
#' patterns except that the ISO 8601 UTC indicator `Z` *will not* be used when
#' the local time offset is `0`.
#'
#' | Field Patterns | Output | Notes |
#' |----------------|------------|-------------------------------------------|
#' | `"x"` | `"-07"` | ISO 8601 basic format (h, optional m) |
#' | `"xx"` | `"-0700"` | ISO 8601 basic format (h & m) |
#' | `"xxx"` | `"-07:00"` | ISO 8601 extended format (h & m) |
#' | `"xxxx"` | `"-0700"` | ISO 8601 basic format (h & m, optional s) |
#' | `"xxxxx"` | `"-07:00"` | ISO 8601 extended format (h & m, optional s) |
#'
#' @param input A vector of date, time, or datetime values. Several
#' representations are acceptable here including strings, `Date` objects, or
#' `POSIXct` objects. Refer to the *Valid Input Values* section for more
#' information.
#' @param format The formatting string to apply to all `input` values. If not
#' provided, the inputs will be formatted to ISO 8601 datetime strings. The
#' *Date/Time Format Syntax* section has detailed information on how to create
#' a formatting string.
#' @param use_tz A tzid (e.g., `"America/New_York"`) time-zone designation for
#' precise formatting of related outputs. This overrides any time zone
#' information available in `character`-based input values and is applied to
#' all vector components.
#' @param locale The output locale to use for formatting the input value
#' according to the specified locale's rules. Example locale names include
#' `"en"` for English (United States) and `"es-EC"` for Spanish (Ecuador). If
#' a locale isn't provided the `"en"` locale will be used. The
#' [fdt_locales_vec] vector contains the valid locales and [fdt_locales_lst]
#' list provides an easy way to obtain a valid locale.
#'
#' @return A character vector of formatted dates, times, or datetimes.
#'
#' @section Examples:
#'
#' ## Basics with `input` datetimes, formatting strings, and localization
#'
#' With an input datetime of `"2018-07-04 22:05"` supplied as a string, we can
#' format to get just a date with the full year first, the month abbreviation
#' second, and the day of the month last (separated by hyphens):
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05",
#' format = "y-MMM-dd"
#' )
#' ```
#' ```
#' #> [1] "2018-Jul-04"
#' ```
#'
#' There are sometimes many options for each time part. Instead of using
#' `"y-MMM-dd"`, let's try a variation on that with `"yy-MMMM-d"`:
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05",
#' format = "yy-MMMM-d"
#' )
#' ```
#' ```
#' #> [1] "18-July-4"
#' ```
#'
#' The output is localizable and so elements will be translated when supplying
#' the appropriate locale code. Let's use `locale = es` to get the month written
#' in Spanish:
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05",
#' format = "yy-MMMM-d",
#' locale = "es"
#' )
#' ```
#' ```
#' #> [1] "18-julio-4"
#' ```
#'
#' `POSIXct` or `POSIXlt` datetimes can serve as an `input` to `fdt()`. Let's
#' create a single datetime value where the timezone is set as `"Asia/Tokyo"`.
#'
#' ```r
#' fdt(
#' input = lubridate::ymd_hms("2020-03-15 19:09:12", tz = "Asia/Tokyo"),
#' format = "EEEE, MMMM d, y 'at' h:mm:ss B (VVVV)"
#' )
#' ```
#' ```
#' #> [1] "Sunday, March 15, 2020 at 7:09:12 in the evening (Tokyo Time)"
#' ```
#'
#' If you're going minimal, it's possible to supply an input datetime string
#' without a `format` directive. What this gives us is formatted datetime
#' output that conforms to ISO 8601. Note that the implied time zone is UTC.
#'
#' ```r
#' fdt(input = "2018-07-04 22:05")
#' ```
#' ```
#' #> [1] "2018-07-04T22:05:00Z"
#' ````
#'
#' ## Using locales stored in the [fdt_locales_lst] list
#'
#' The [fdt_locales_lst] object is provided in **bigD** to make it easier to
#' choose one of supported locales. You can avoid typing errors and every
#' element of the list is meant to work. For example, we can use the `"it"`
#' locale by accessing it from [fdt_locales_lst] (autocomplete makes this
#' relatively simple).
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05",
#' format = "yy-MMMM-d",
#' locale = fdt_locales_lst$it
#' )
#' ```
#' ```
#' #> [1] "18-luglio-4"
#' ````
#'
#' ## Omission of date or time in `input`
#'
#' You don't have to supply a full datetime to `input`. Just supplying the date
#' portion implies midnight (and is just fine if you're only going to present
#' the date anyway).
#'
#' ```r
#' fdt(input = "2018-07-04")
#' ```
#' ```
#' #> [1] "2018-07-04T00:00:00Z"
#' ````
#'
#' If you omit the date and just supply a time, `fdt()` will correctly parse
#' this. The current date on the user system will be used because we need to
#' create some sort of datetime value internally. Again, this is alright if
#' you just intend to present a formatted time value.
#'
#' ```r
#' fdt(input = "22:05")
#' ```
#' ```
#' #> [1] "2022-08-16T22:05:00Z"
#' ````
#'
#' To see all of the supported locales, we can look at the vector supplied by
#' the [fdt_locales_vec()] function.
#'
#' ## Using standardized forms with the `standard_*()` helper functions
#'
#' With an input datetime of `"2018-07-04 22:05(America/Vancouver)"`, we can
#' format the date and time in a standardized way with `standard_date_time()`
#' providing the correct formatting string. This function is invoked in the
#' `format` argument of `fdt()`:
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05(America/Vancouver)",
#' format = standard_date_time(type = "full")
#' )
#' ```
#' ```
#' #> [1] "Wednesday, July 4, 2018 at 10:05:00 PM Pacific Daylight Time"
#' ```
#'
#' The locale can be changed and we don't have to worry about the particulars
#' of the formatting string (they are standardized across locales).
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05(America/Vancouver)",
#' format = standard_date_time(type = "full"),
#' locale = fdt_locales_lst$nl
#' )
#' ```
#' ```
#' #> [1] "woensdag 4 juli 2018 om 22:05:00 Pacific-zomertijd"
#' ```
#'
#' We can use different `type` values to control the output datetime string. The
#' default is `"short"`.
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05(America/Vancouver)",
#' format = standard_date_time()
#' )
#' ```
#' ```
#' #> [1] "7/4/18, 10:05 PM"
#' ```
#'
#' After that, it's `"medium"`:
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05(America/Vancouver)",
#' format = standard_date_time(type = "medium")
#' )
#' ```
#' ```
#' #> [1] "Jul 4, 2018, 10:05:00 PM"
#' ```
#'
#' The `"short"` and `"medium"` types don't display time zone information in the
#' output. Beginning with `"long"`, the tz is shown.
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05(America/Vancouver)",
#' format = standard_date_time(type = "long")
#' )
#' ```
#' ```
#' #> [1] "July 4, 2018 at 10:05:00 PM PDT"
#' ```
#'
#' If you don't include time zone information in the input, the `"UTC"` time
#' zone will be assumed:
#'
#' ```r
#' fdt(
#' input = "2018-07-04 22:05",
#' format = standard_date_time(type = "full")
#' )
#' ```
#' ```
#' #> [1] "Wednesday, July 4, 2018 at 10:05:00 PM GMT+00:00"
#' ```
#'
#' ## Using flexible date and time (12- and 24-hour) formatting
#'
#' The **bigD** package supplies a set of lists to allow for flexible date and
#' time formatting ([flex_d_lst], [flex_t24_lst], and [flex_t12_lst]). These
#' are useful when you need a particular format that works across all locales.
#' Here's an example that uses the `"yMMMEd"` flexible date type by accessing it
#' from the `flex_d_lst` object, yielding a formatted date.
#'
#' ```r
#' fdt(
#' input = "2021-01-09 16:32(America/Toronto)",
#' format = flex_d_lst$yMMMEd,
#' )
#' ```
#' ```
#' #> [1] "Sat, Jan 9, 2021"
#' ```
#'
#' If we wanted this in a different locale, the locale-specific `format` pattern
#' behind the flexible date identifier would ensure consistency while moving to
#' that locale.
#'
#' ```r
#' fdt(
#' input = "2021-01-09 16:32(America/Toronto)",
#' format = flex_d_lst$yMMMEd,
#' locale = "fr_CA"
#' )
#' ```
#' ```
#' #> [1] "sam. 9 janv. 2021"
#' ```
#'
#' Formatting as a 12-hour time with the [flex_t12_lst] list and using the
#' `"hms"` flexible type:
#'
#' ```r
#' fdt(
#' input = "2021-01-09 16:32(America/Toronto)",
#' format = flex_t12_lst$hms
#' )
#' ```
#' ```
#' #> [1] "4:32:00 PM"
#' ```
#'
#' The 24-hour variant, [flex_t24_lst], has a similar `"Hms"` flexible type that
#' will give us a 24-hour version of the same clock time:
#'
#' ```r
#' fdt(
#' input = "2021-01-09 16:32(America/Toronto)",
#' format = flex_t24_lst$Hms
#' )
#' ```
#' ```
#' #> [1] "16:32:00"
#' ```
#'
#' A flexible date and time can be used together by enveloping the two in a
#' list (**bigD** will handle putting the date and time together in a sensible
#' manner).
#'
#' ```r
#' fdt(
#' input = "2021-01-09 16:32(America/Toronto)",
#' format = list(flex_d_lst$yMMMEd, flex_t24_lst$Hmv)
#' )
#' ```
#' ```
#' #> "Sat, Jan 9, 2021, 16:32 ET"
#' ```
#'
#' @export
fdt <- function(
input,
format = NULL,
use_tz = NULL,
locale = NULL
) {
# TODO: Validate locale
if (!is.null(locale)) {
locale <- gsub("_", "-", locale, fixed = TRUE)
}
locale <- locale %||% "en"
if (inherits(input, "POSIXlt")) {
input <- as.POSIXct(input)
}
if (inherits(input, "Date")) {
input <- as.POSIXct(as.POSIXlt(input))
}
if (is.null(format)) {
format <- "yyyy-MM-dd'T'HH:mm:ssXXX"
} else if (
length(format) == 1 &&
is.character(format) &&
!inherits(format, "date_time_pattern")
) {
NULL
} else if (
length(format) == 1 &&
is.character(format) &&
inherits(format, "date_time_pattern") &&
!inherits(format, "standard")
) {
format <- amend_week_pattern(format = format, input = input)
format <- dates[dates$locale == locale, ][["date_time_available_formats"]][["value"]][[format]]
} else if (
length(format) == 1 &&
is.character(format) &&
inherits(format, "date_time_pattern") &&
inherits(format, "standard")
) {
dt_types <- base::setdiff(class(format), c("date_time_pattern", "standard"))
type_date_time <- dt_types[1]
type_size <- dt_types[2]
date_format <-
dates[dates$locale == locale, ][["date_formats"]][["value"]][[type_size]]
time_format <-
dates[dates$locale == locale, ][["time_formats"]][["value"]][[type_size]]
combining_format <-
dates[dates$locale == locale, ][["date_time_patterns"]][["value"]][[type_size]]
if (type_date_time == "date_time") {
format <- gsub("{1}", date_format, combining_format, fixed = TRUE)
format <- gsub("{0}", time_format, format, fixed = TRUE)
} else if (type_date_time == "date") {
format <- date_format
} else {
format <- time_format
}
} else if (
is.list(format) &&
length(format) == 2 &&
inherits(format[[1]], "date_time_pattern") &&
inherits(format[[2]], "date_time_pattern")
) {
if (inherits(format[[1]], "flex_d")) {
format_1 <- "date"
} else {
format_1 <- "time"
}
if (inherits(format[[2]], "flex_d")) {
format_2 <- "date"
} else {
format_2 <- "time"
}
if (identical(format_1, format_2)) {
stop(
"When supplying two flexible formats, one must represent a date and ",
"the other a time",
call. = FALSE
)
}
date_format <- if (format_1 == "date") format[[1]] else format[[2]]
time_format <- if (format_2 == "time") format[[2]] else format[[1]]
date_format <- amend_week_pattern(format = date_format, input = input)
combine_length <- "medium"
date_format <-
dates[dates$locale == locale, ][["date_time_available_formats"]][["value"]][[date_format]]
time_format <-
dates[dates$locale == locale, ][["date_time_available_formats"]][["value"]][[time_format]]
combining_format <-
dates[dates$locale == locale, ][["date_time_patterns"]][["value"]][[combine_length]]
format <- gsub("{1}", date_format, combining_format, fixed = TRUE)
format <- gsub("{0}", time_format, format, fixed = TRUE)
}
dt_out <- rep_len(NA_character_, length(input))
# Verify if each entry has timezone (checking before loop for performance)
if (is.character(input)) {
tz_present <- is_tz_present(input = input)
}
for (i in seq_along(input)) {
input_i <- input[i]
# Initialize an empty `tz_info` list
tz_info <-
list(
tz_str = NA_character_,
tz_offset = NA_real_,
long_tzid = NA_character_,
tz_short_specific = NA_character_,
tz_long_specific = NA_character_
)
# Modify the `format` string so it can be more precisely formatted
pattern_list <- dt_format_pattern(format = format)
if (is.character(input_i)) {
date_present <- is_date_present(input = input_i)
time_present <- is_time_present(input = input_i)
# Determine if tz information is present, either as:
# [1] a tz offset in hours from GMT (defined above before loop)
# [2] a long tz identifier (either canonical or an alias)
tz_present_i <- tz_present[i]
long_tzid_present <- is_long_tzid_present(input = input_i)
# Strip away tz information from the input and return as `input_str`
if (tz_present_i) {
input_str <- strip_tz(input = input_i)
} else if (long_tzid_present) {
input_str <- strip_long_tzid(input = input_i)
} else {
input_str <- input_i
}
# Obtain the date and time
if (date_present) {
input_dt <- as.POSIXct(gsub("T", " ", input_str, fixed = TRUE), tz = "UTC")
} else if (!date_present && time_present) {
date_now <- as.character(Sys.Date())
input_str <- paste0(date_now, "T", input_str)
input_dt <- as.POSIXct(gsub("T", " ", input_str, fixed = TRUE), tz = "UTC")
} else if (!date_present && !time_present) {
if (nchar(input_str) == 4 && grepl("^[0-9]{4}$", input_str)) {
# Case where only a four-digit C.E. year is provided
input_str <- paste0(input_str, "-01-01")
input_dt <- as.POSIXct(input_str, tz = "UTC")
}
if (nchar(input_str) == 7 && grepl("^[0-9]{4}-[0-9]{2}$", input_str)) {
# Case where the year and month are provided in the YYYY-MM format
input_str <- paste0(input_str, "-01")
input_dt <- as.POSIXct(input_str, tz = "UTC")
}
if (grepl("W", input_str, fixed = TRUE)) {
# Case where week dates are possibly provided in one of
# several permitted ISO 8601 formats
# Perform check of `input_str` to confirm that week dates
# conform to one of four ISO 8601 specifications:
# (1) YYYY-Www
# (2) YYYYWww
# (3) YYYY-Www-D
# (4) YYYYWwwD
if (!grepl("^([0-9]{4}-?W[0-9]{2}|[0-9]{4}-?W[0-9]{2}-?[1-7])$", input_str)) {
stop(
"The provided input '", input_str, "' does not conform to a week ",
"date representation.",
call. = FALSE
)
}
# Remove hyphens if any are present
input_str <- gsub("-", "", input_str, fixed = TRUE)
input_dt <- week_date_as_datetime(input_str = input_str)
}
if (startsWith(input_str, "--")) {
if (!grepl("^--[0-9]{2}-?[0-9]{2}$", input_str)) {
stop(
"The provided input '", input_str, "' does not conform to a ",
"month-day representation.",
call. = FALSE
)
}
# Remove all hyphens from `input_str`
input_str <- gsub("-", "", input_str, fixed = TRUE)
input_dt <- month_day_as_datetime(input_str = input_str)
}
if (grepl("[0-9]+\\s?(BC|AD|BCE|CE|B\\.C\\.E\\.|C\\.E\\.|EV)$", input_str)) {
# Remove any spaces or periods from `input_str`
input_str <- gsub(" ", "", input_str, fixed = TRUE)
input_str <- gsub(".", "", input_str, fixed = TRUE)
year <- as.integer(gsub("([0-9]+).*", "\\1", input_str))
era <- gsub("[0-9]+", "", input_str)
if (any(c("BC", "BCE") %in% era)) {
year <- year * (-1)
}
adjustment <- -1900L
posixlt_dt <- as.POSIXlt("0000-01-01", tz = "UTC")
posixlt_dt$year <- year + adjustment
input_dt <- as.POSIXct(posixlt_dt)
}
# TODO: Add parsers for different date/time cases
}
# Derive more detailed time zone information from the `long_tzid` value
if (!is.null(use_tz)) {
# Validate and then normalize the provided time zone
validate_long_tzid(long_tzid = use_tz)
long_tzid <- normalize_long_tzid(long_tzid = use_tz)
tz_info$long_tzid <- long_tzid
tz_info$tz_str <- long_tzid_to_tz_str(long_tzid = tz_info$long_tzid, input_dt = input_dt)
tz_info$tz_offset <- get_tz_offset_val_from_tz_str(tz_str = tz_info$tz_str)
tz_info$tz_short_specific <- get_tz_short_specific(long_tzid = tz_info$long_tzid, input_dt = input_dt)
tz_info$tz_long_specific <- get_tz_long_specific(long_tzid = tz_info$long_tzid, input_dt = input_dt, locale = locale)
} else if (long_tzid_present) {
tz_info$long_tzid <- get_long_tzid_str(input = input_i)
tz_info$tz_str <- long_tzid_to_tz_str(long_tzid = tz_info$long_tzid, input_dt = input_dt)
tz_info$tz_offset <- get_tz_offset_val_from_tz_str(tz_str = tz_info$tz_str)
tz_info$tz_short_specific <- get_tz_short_specific(long_tzid = tz_info$long_tzid, input_dt = input_dt)
tz_info$tz_long_specific <- get_tz_long_specific(long_tzid = tz_info$long_tzid, input_dt = input_dt, locale = locale)
} else if (tz_present_i) {
tz_info$tz_str <- get_tz_str(input = input_i)
tz_info$tz_offset <- get_tz_offset_val(input = input_i)
} else {
tz_info$long_tzid <- "UTC"
tz_info$tz_str <- "GMT"
tz_info$tz_offset <- 0L
tz_info$tz_short_specific <- get_tz_short_specific(long_tzid = tz_info$long_tzid, input_dt = input_dt)
}
}
if (inherits(input, "POSIXct")) {
input_dt <- input_i
# Extract the input time zone
long_tzid <- attr(input_dt, which = "tzone", exact = TRUE)
offset_val <- as.POSIXlt(input_dt)$gmtoff / 3600
if (!is.null(long_tzid) && long_tzid == "UTC") {
tz_info$long_tzid <- long_tzid
tz_info$tz_str <- "GMT"
tz_info$tz_offset <- 0L
tz_info$tz_short_specific <- get_tz_short_specific(long_tzid = tz_info$long_tzid, input_dt = input_dt)
tz_info$tz_long_specific <- get_tz_long_specific(long_tzid = tz_info$long_tzid, input_dt = input_dt, locale = locale)
} else if (is.null(long_tzid)) {
tz_info$long_tzid <- NA_character_
tz_info$tz_str <- "GMT"
tz_info$tz_offset <- offset_val
} else {
tz_info$long_tzid <- long_tzid
tz_info$tz_str <- long_tzid_to_tz_str(long_tzid = tz_info$long_tzid, input_dt = input_dt)
tz_info$tz_offset <- get_tz_offset_val_from_tz_str(tz_str = tz_info$tz_str)
tz_info$tz_short_specific <- get_tz_short_specific(long_tzid = tz_info$long_tzid, input_dt = input_dt)
tz_info$tz_long_specific <- get_tz_long_specific(long_tzid = tz_info$long_tzid, input_dt = input_dt, locale = locale)
}
}
# Replace format "{yyyy}-{MM}-{dd}'1'{HH}:{mm}:{ss}{XXX}"
# by the corresponding value
dt <- dt_replace(
# example "{yyyy}-{MM}-{dd}'1'{HH}:{mm}:{ss}{XXX}"
dt = pattern_list$format,
input_dt = input_dt,
dt_lett = pattern_list$dt_letters,
locale = locale,
tz_info = tz_info
)
dt <- gsub("\\{(.*?)\\}", "\\1", dt)
# Replace string literal markers `'<#>'` with captured literal values
for (j in seq_along(pattern_list$literals)) {
dt <-
gsub(
paste0("'", j, "'"),
pattern_list$literals[j],
dt,
fixed = TRUE
)
}
# Replace each instance of `''` with `'`
dt <- gsub("''", "'", dt, fixed = TRUE)
dt_out[i] <- dt
}
dt_out
}
sub_letters <- function() {
c(
"G",
"y",
"Y",
"u",
"U",
"r",
"Q",
"q",
"M",
"L",
"w",
"W",
"d",
"D",
"F",
"g",
"E",
"e",
"c",
"a",
"b",
"B",
"h",
"H",
"K",
"k",
"m",
"s",
"S",
"A",
"z",
"Z",
"O",
"v",
"V",
"X",
"x"
)
}
extract_literals_from_pattern <- function(string) {
gsub(
"(^'|'$)", "",
unlist(regmatches(string, gregexpr(pattern = "'.*?'", text = string)))
)
}
dt_format_pattern <- function(format) {
literals <- extract_literals_from_pattern(string = format)
for (i in seq_along(literals)) {
if (nzchar(literals[i])) {
format <- sub(paste0("'", literals[i], "'"), paste0("'", i, "'"), format)
}
}
dt_letters <-
base::intersect(
unique(unlist(strsplit(format, "", fixed = TRUE))),
sub_letters()
)
# Bad error if length(dt_letters) == 0
pattern <- paste0("(", paste0("[", dt_letters, "]+", collapse = "|"), ")")
format <- gsub(pattern, "\\{\\1\\}", format)
literals <- gsub("(^'|'$)", "", literals)
list(
format = format,
literals = literals,
dt_letters = dt_letters
)
}
amend_week_pattern <- function(format, input) {
if (format == "MMMMW" || format == "yw") {
year_week <- format_yearweek(input = input)
if (grepl("W01", year_week, fixed = TRUE)) {
format <- paste0(format, "-count-one")
} else {
format <- paste0(format, "-count-other")
}
}
format
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.