最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How parse a date string generated from Intl.DateTimeFormat - Stack Overflow

programmeradmin2浏览0评论

Using Intl.DateTimeFormat.format function you can generate a date string formatted for a specific locale. The options passed to the Intl.DateTimeFormat.format() function allow to know some things about the format, for example if the year is two digit or four digit, but some things are not known, for example the separator used or the order of the year, month and day elements.

Trying to parse that string back to a Date object is not always possible using Date.parse.

For example this code fails for the spanish locale and works for the english:

const date = new Date(2020, 10, 28);

const regionEs = new Intl.DateTimeFormat('es', { timeZone: 'UTC' });
const regionEn = new Intl.DateTimeFormat('en', { timeZone: 'UTC' });

const stringEs = regionEs.format(date); // "28/11/2020"
const stringEn = regionEn.format(date); // "11/28/2020"

const parseEs = new Date(Date.parse(stringEs)); // Error -> Trying to set month to 28
const parseEn = new Date(Date.parse(stringEn)); // Ok

But it could be easy if the format template used to generate the string could be obtained from Intl: something like "dd/mm/yyyy". This way the string could be safely splited into parts that could be used to build a Date object. The problem is that it seems not possible to get that information from Intl.DateTimeFormat.

The [Intl.resolvedOptions()][1] method does not provide any help since it just provides back the options passed on the constructor plus defaults.

Question

Is there any way to parse back a string formatted with Intl to a Date object without using moment.js or any other external library?

My use case

I'm using a date-time ponent that accepts a format and parse functions to handle the dates. When the user selects a date using the calendar controls of the date time input there is no problem and the format function uses Intl to format it according to the locale and a set of format options. Sometimes the user edit the date displayed manually. If spanish locale is used he can see "27/11/2020" date displayed and he made decide to change the day to "28/11/2020". This will fail because Date.parse() can not parse this date (see above). This may get worse in other locales. I'm trying to avoid including an external date library, but don't see a way to overe this. I know that the user may edit the date in any arbitrary format, but I would like to accept at least the same format that is displayed in the control, which I think is the most UI friendly, since there is always a default date displayed.

Using Intl.DateTimeFormat.format function you can generate a date string formatted for a specific locale. The options passed to the Intl.DateTimeFormat.format() function allow to know some things about the format, for example if the year is two digit or four digit, but some things are not known, for example the separator used or the order of the year, month and day elements.

Trying to parse that string back to a Date object is not always possible using Date.parse.

For example this code fails for the spanish locale and works for the english:

const date = new Date(2020, 10, 28);

const regionEs = new Intl.DateTimeFormat('es', { timeZone: 'UTC' });
const regionEn = new Intl.DateTimeFormat('en', { timeZone: 'UTC' });

const stringEs = regionEs.format(date); // "28/11/2020"
const stringEn = regionEn.format(date); // "11/28/2020"

const parseEs = new Date(Date.parse(stringEs)); // Error -> Trying to set month to 28
const parseEn = new Date(Date.parse(stringEn)); // Ok

But it could be easy if the format template used to generate the string could be obtained from Intl: something like "dd/mm/yyyy". This way the string could be safely splited into parts that could be used to build a Date object. The problem is that it seems not possible to get that information from Intl.DateTimeFormat.

The [Intl.resolvedOptions()][1] method does not provide any help since it just provides back the options passed on the constructor plus defaults.

Question

Is there any way to parse back a string formatted with Intl to a Date object without using moment.js or any other external library?

My use case

I'm using a date-time ponent that accepts a format and parse functions to handle the dates. When the user selects a date using the calendar controls of the date time input there is no problem and the format function uses Intl to format it according to the locale and a set of format options. Sometimes the user edit the date displayed manually. If spanish locale is used he can see "27/11/2020" date displayed and he made decide to change the day to "28/11/2020". This will fail because Date.parse() can not parse this date (see above). This may get worse in other locales. I'm trying to avoid including an external date library, but don't see a way to overe this. I know that the user may edit the date in any arbitrary format, but I would like to accept at least the same format that is displayed in the control, which I think is the most UI friendly, since there is always a default date displayed.

Share Improve this question edited Oct 28, 2020 at 9:02 David Casillas asked Oct 28, 2020 at 8:56 David CasillasDavid Casillas 1,9211 gold badge30 silver badges57 bronze badges 6
  • 2 You should always work with a known (preferably standardised) format like ISO 8601 and only use other formats for presentation. Parsing arbitrarily formatted dates is fraught and bound to cause issues, see Why does Date.parse give incorrect results? If you don't know the format and can't supply it to the parser, then a library is no better than Date.parse (and most will fall back to it if the format is not specified and doesn't fit one of its supported formats). – RobG Commented Oct 28, 2020 at 23:12
  • So the basic question is how do you get the format given a locale and a set of options (i.e. 4 digit year)? That info must be somewhere since Intl.DateTimeFormat uses it to format the date. – David Casillas Commented Oct 29, 2020 at 9:12
  • 1 The relevant standard is ECMA-402, it doesn't provide a mapping of language to format. The information you seek is encoded in each implementation and is not necessarily consistent between them. However, ECMA-402 remends that implementations use the language to format mapping suggested by the Unicode Common Locale Data Repository. You might try the ICU Locale Explorer. – RobG Commented Oct 29, 2020 at 10:40
  • 1 I think it's a Sisyphean task. Even using just the language without options, there are a huge number of variants. Adding options changes the format in inconsistent ways, e.g. here. – RobG Commented Oct 29, 2020 at 10:53
  • I see the only workaround is to change the view to the ISO 8601 format when the user tries to manually edit the date, so at least he can see what the expected format is, and then switch to the locale view when he finishes editing. – David Casillas Commented Oct 29, 2020 at 11:11
 |  Show 1 more ment

3 Answers 3

Reset to default 7

I ran into this issue building a date picker which needed to both display selected dates in the browser's locale and parse dates manually typed by the user in their own locale. A simplified version of my solution is here:

function getDateFormat(locale = undefined) {
  const formatted = new Intl.DateTimeFormat(locale).format(new Date(2000, 0, 2));
  return formatted
    .replace('2000', 'YYYY')
    .replace('01', 'MM')
    .replace('1', 'M')
    .replace('02', 'DD')
    .replace('2', 'D');
}

function getFormattedDateRegex(format) {
  return new RegExp(
      '^\\s*' + format.toUpperCase().replaceAll(/([MDY])\1*/g, '(?<$1>\\d+)') + '\\s*$'
    );
}

function parseFormattedDate(value, locale = undefined) {
  const format = getDateFormat(locale);
  const regex = getFormattedDateRegex(format);

  const { groups } = value.match(regex) ?? {};

  if (!groups) return null;

  const y = Number(groups.Y);
  const m = Number(groups.M);
  const d = Number(groups.D);

  // Validate range of year and month
  if (y < 1000 || y > 2999) return null;
  if (m < 1 || m > 12) return null;

  const date = new Date(y, m - 1, d);

  // Validate day of month exists
  if (d !== date.getDate()) return null;

  return isNaN(date.valueOf()) ? null : date;
}

getDateFormat generates a known date, formats it in the requested locale (or the browser locale), and then uses the output to determine what format Intl is using.

getFormattedDateRegex takes a format string (e.g. 'MM/DD/YYYY`) and creates a regex with named capture groups for parsing dates in that format.

parseFormattedDate simply takes a formatted date string (and optionally a locale) and attempts to parse it. It returns null if the date string does not appear to match the format Intl uses in the current locale.

This is hard to get right :(

I came up with these two functions that also deal with short year and optional time:

// format local date using locale (ie "dd/mm/yyyy" or "mm/dd/yyyy")
function formatLocalDate(dateObj, withTime=true, shortYear=false) {
    if (!dateObj) return ''
    const locale = Intl.DateTimeFormat().resolvedOptions().locale
    let date = new Intl.DateTimeFormat(locale, { dateStyle: "short" }).format(dateObj)
    let time = new Intl.DateTimeFormat(locale, { timeStyle: "short", hour12: false }).format(dateObj)
    if (shortYear && date.length == 10) date = date.substr(0, 6) + date.substr(8)
    return withTime ? date+' '+time : date
}

// parse a date string that was created with formatDate
function parseLocalDate(dateStr, withTime = true, shortYear = false) {
    let f = formatLocalDate(new Date(2022, 11 - 1, 12, 13, 14), withTime, shortYear)
    let regEx = shortYear ? f.replace('22', '(?<year>\\d\\d)') : f.replace('2022', '(?<year>\\d\\d\\d\\d)') 
    regEx = regEx.replace('11', '(?<month>\\d\\d*)').replace('12', '(?<day>\\d\\d*)').replace('13', '(?<hour>\\d\\d*)').replace('14', '(?<min>\\d\\d*)')
    regEx = `^\\s*${regEx}\\s*$`
    const { groups } = dateStr.match(regEx) ?? {};
    if (!groups) return null;
    let year = parseInt(groups.year), month = parseInt(groups.month), day = parseInt(groups.day), hour = withTime ? parseInt(groups.hour) : 0, min = withTime ? parseInt(groups.min) : 0  
    if (year < 100) year += 2000
    const date = new Date(year, month - 1, day, hour, min);
    return isNaN(date.valueOf()) ? null : date;
}   

// test
const d = new Date(2023, 8, 13, 21, 35)
console.log(`\n${d}\n${formatLocalDate(d)}\n${parseLocalDate(formatLocalDate(d))}`)
console.log(`\n${d}\n${formatLocalDate(d, false)}\n${parseLocalDate(formatLocalDate(d, false), false)}`)
console.log(`\n${d}\n${formatLocalDate(d, true, true)}\n${parseLocalDate(formatLocalDate(d, true, true), true, true)}`)

Sure you can use moment.js for such a thing, but you need to know the format in every region you are using, according to your code

const parseEs = moment("28/11/2020", "DD/MM/YYYY");
const parseEn = moment("11/28/2020", "MM/DD/YYYY");

In the end moment provides toDate() function so you can pass it again to region format (in case you add/subtract or edit the date with any ponent or library)

const stringEs = regionEs.format(parseEs.toDate()); // "28/11/2020"
const stringEn = regionEn.format(parseEn.ToDate()); // "11/28/2020"
发布评论

评论列表(0)

  1. 暂无评论