对应于IANA时区名称的用户友好时区名称

7

我正在尝试寻找一个用户友好的时区列表,供用户选择他们当前所在的时区。它将包含常见的时区名称,比如太平洋时间(美国和加拿大)

这个列表应该将文本映射到IANA标准名称,比如America/Los_Angeles

它还应该移除用户认为是“相同”的时区列表。例如,太平洋时间(美国和加拿大)对应于America/Los_Angeles和America/Tijuana,它们是“相同”的时区。

我已经尝试查看IANA时区数据库ECMA脚本国际化API和类库,如Moment.js。它们可以将IANA名称解析为通用名称,例如America/Los_Angeles ==> "Pacific Standard Time",但没有内置函数列出所有通用名称及其相应的IANA名称。


你打算如何使用这个名字列表?这个问题的答案将有助于确定检索和处理所需列表的最佳方法。例如,如果您想要一次性转储,可能最简单的方法是将当前的IANA名称列表和常用名称导入到数据库中,并以所需格式导出数据。另一方面,如果您想要一个始终保持最新的列表,您将不得不找到提供信息的API。 - kmoser
@kmoser 一次性的转储很好。这些名称将用作下拉选项。当用户在我的网站上注册时,我会检测他们的时区以设置一个默认选项,如果需要,他们可以更新它。容易的部分是检测时区,这是一个简单的API调用。棘手的部分是将所有300多个IANA名称映射到100个简化的下拉选项。这很棘手,因为一个下拉选项对应于许多IANA字符串。 - nth-child
1
@kmoser 我在Zoom API文档中找到了这个IANA名称和通用名称的表格:https://marketplace.zoom.us/docs/api-reference/other-references/abbreviation-lists#timezones但是它漏掉了很多IANA名称。如果检测到的时区与这些ID不匹配会发生什么?你会编写规则来处理吗?那个规则会是什么? - nth-child
@yogadog - 那个列表几乎完全没有时区名称。它只包括少数标准的北美常用名称,没有夏令时常用名称。 - RobG
@RobG 确实如此。但是下拉选项≠时区名称。例如,当用户选择他的“时区”时,他不会从“太平洋夏令时”和“太平洋标准时间”中进行选择。相反,下拉列表使用一个标识符,例如Pacific (US & Canada),并在适当时打印他的约会时间为PDT和PST。下拉选项不需要以时区名称命名。目标是创建一个合理的下拉列表供用户选择。有任何想法吗? - nth-child
实际上并没有那么多的时区。有26个整点偏移且没有夏令时的时区,大约有10个带有分数偏移的时区,还有另外12个在北美带有夏令时规则,以及大约11个在国外带有夏令时规则,就是这些了。你可以做的一件事是通过打开命令提示符并输入tzutil /l > C:/path/to/time_zones.txt来导出Windows时区列表。然后逐个更改计算机的系统时间到每个时区,并打开Chrome的JavaScript控制台,通过输入Intl.DateTimeFormat().resolvedOptions().timeZone [Enter]获取每个时区的IANA名称。 - PHP Guru
3个回答

3
您可以使用 Intl.DateTimeFormat.prototype.formatToParts()
new Intl.DateTimeFormat(
    'default',
    { timeZone: 'America/Los_Angeles', timeZoneName: 'long' }
).formatToParts().find(({ type }) => type === 'timeZoneName').value
// 'Pacific Daylight Time'

这意味着您可以使用此代码查询每个IANA时区的浏览器,并可能为所有相关时区构建人类友好字符串的数据库。 - Pathogen
转念一想:有一个重要的警告,即这些长/短时区名称将反映浏览器中运行时的标准/夏令时。毕竟并不是那么干净的解决方案。 - Pathogen

3
简短的回答是没有简单的从IANA代表位置名称映射到常用名称的地图,因为IANA名称不反映历史上偏移或夏令时的更改,而且常用名称也没有被正式或事实上充分标准化。
长的回答是,IANA代表位置包括本地时间和夏令时的历史偏移变化。标准的时区名称可能没有改变,但是偏移量已经改变并将继续改变。此外,IANA代表位置名称在夏令时期间保持不变,但是偏移和常用名称却不同。
例如,萨摩亚的IANA名称为“Pacific / Samoa”。2011年12月29日之前的标准偏移量为UTC−11:00,之后它的偏移量为UTC + 13:00,并且其夏令时从UTC-1000变为UTC + 14:00。其时区的常用名称为“萨摩亚标准时间”和“萨摩亚夏令时”,但是这些名称并没有被标准化,在许多地方都存在歧义(例如CST可能是2或3个不同地方的中央标准时间或中国标准时间)。
因此,并非所有IANA位置的代表位置名称都有一个简单的地图映射到常用时区名称。您可能能够为一小部分创建一个地图,但使用内置的Intl对象可能更简单,例如:

let opts = {
  hour: 'numeric',
  minute:'2-digit',
  timeZone: 'Pacific/Samoa',
  timeZoneName: 'long'
};
let time = new Date().toLocaleString('en', opts);
console.log('Currently in Samoa it\'s ' + time);

注意

在最新版本的MacOS上,以上代码返回萨摩亚岛的UTC-1100偏移量,仍然使用旧的值,并且不识别夏令时,因此不能完全依赖Intl对象。

您可以像下面这样使用Intl对象,需要注意对于某些位置,它可能没有时区名称,只显示偏移量(例如尝试Europe/Astrakhan)。有近600个IANA位置可供选择。

// Detect user representative location if available
let repLoc = (typeof Intl == 'object' && 
              typeof Intl.DateTimeFormat == 'function' &&
              typeof Intl.DateTimeFormat().resolvedOptions == 'function')?
              Intl.DateTimeFormat().resolvedOptions().timeZone : 'default';

function setRepLoc(evt) {
  showSampleDates(this.value);
}

function showSampleDates(repLoc) {
  let el0 = document.getElementById('sDate0');
  let el1 = document.getElementById('sOff0');
  let el2 = document.getElementById('sDate1');
  let el3 = document.getElementById('sOff1');

  let d0 = new Date(Date.UTC(2020,0,1,11));
  let d1 = new Date(Date.UTC(2020,5,1,11));
  let opts = {
    weekday: 'short',
    day: 'numeric',
    month: 'short',
    year: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    timeZone: repLoc,
    timeZoneName: 'long'
  };
  let offOpts = {
    hour: 'numeric',
    hour12: false,
    timeZone: repLoc,
    timeZoneName: 'short'
  }
  if (repLoc == 'default') {
    el0.textContent = '';
    el1.textContent = '';
    el2.textContent = '';
    el3.textContent = '';
  } else {
    el0.textContent = d0.toLocaleString('en-NZ', opts);
    el1.textContent = d0.toLocaleString('en-NZ', offOpts).split(' ')[1];
    el2.textContent = d1.toLocaleString('en-NZ', opts);
    el3.textContent = d1.toLocaleString('en-NZ', offOpts).split(' ')[1];
  }
}

window.onload = function(){
  let sel = document.getElementById('repLoc')
  if (repLoc) {
    sel.options[0].text = repLoc;
    sel.options[0].value = repLoc;
    sel.selectedIndex = 0;
    setRepLoc.call(sel);
  }
  sel.addEventListener(
    'change',
    setRepLoc,
    false);
};
td:nth-child(1){text-align:right;color:#999;}
td:nth-child(2){font-family:monospace}
<table>
  <tr>
    <td>IANA loc:
    <td><select id="repLoc">
          <option selected value="default">Select a location
          <option>Europe/Amsterdam
          <option>Europe/Andorra
          <option>Europe/Astrakhan
          <option>Europe/Athens
          <option>Europe/Belfast
          <option>Pacific/Samoa
          <option>Pacific/Kiritimati
          <option>Antarctica/DumontDUrville
          <option>Antarctica/Davis
          <option>America/Chicago
          <option>Australia/Brisbane
        </select>
  <tr>
    <td>Sample:
    <td id="sDate0">
  <tr>
    <td>Offset:
    <td id="sOff0">
  <tr>
    <td>Sample:
    <td id="sDate1">
  <tr>
    <td>Offset:
    <td id="sOff1">

</table>


谢谢!这很有帮助。当用户在我的网站上注册时,我会自动检测他们的时区,以便为他们设置一个合理的默认时区。但是如果Intl对象检测到的时区与我的时区下拉选项不匹配怎么办?我该如何处理? - nth-child
1
听起来像是另一个问题。可以要求用户提供他们的地理位置,然后将其映射到 IANA 代表性位置,或者直接给他们选择 IANA 位置的选项。始终反馈结果,例如立即显示带有偏移和时区名称的日期和时间。然后用户可以确认它是否合适并更改他们的选择,直到他们得到正确的结果。您还应该显示一个偏移了 6 个月的时区名称,以显示 DST 时区名称的外观(准备好可能会出错)。 - RobG

2

在Kimamula的回答基础上,我使用了当前的IANA时区名称列表,并通过我的浏览器的Intl格式化程序获取了它们的长名称和短名称。结果是一个gist,其中包含IANA名称与人类可读名称的映射。我选择了这些名称的“通用”形式,省略了夏令时的提及。否则,我们需要知道是否实行夏令时来选择名称。(例如,Pacific Time而不是Pacific Standard Time或Pacific Daylight Time)。

并非所有时区在此数据库中都有理想的名称,像大多数与时间相关的事物一样,这将是一个不完美的解决方案。但它希望能涵盖您的用户最常使用的时区,并显示IANA名称作为备选项。

如果您想自己生成这些名称,可以在浏览器中运行此操作。这些名称来自Firefox,我猜它们是实现相关的,在其他浏览器中可能会有所不同。


const iana_names = [
  "Africa/Abidjan",
  "Africa/Accra",
  "Africa/Addis_Ababa",
  // ...
  "W-SU",
  "WET",
  "Zulu"
]

const mapping = {}

iana_names.forEach( name => {

   const long = new Intl.DateTimeFormat('default', { timeZone: name, timeZoneName: 'longGeneric' }).formatToParts().find(({ type }) => type === 'timeZoneName').value
   const short = new Intl.DateTimeFormat('default', { timeZone: name, timeZoneName: 'shortGeneric' }).formatToParts().find(({ type }) => type === 'timeZoneName').value

   mapping[name] = {
     long, short
   }

})

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接