timezone.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. var dataUtils = require('utils/data_manipulation');
  19. /**
  20. * Transitional list of timezones (used to create list of shownTimezone @see shownTimezone)
  21. *
  22. * <code>utcOffset</code> - offset-value (0, 180, 240 etc)
  23. * <code>formattedOffset</code> - formatted offset-value ('+00:00', '-02:00' etc)
  24. * <code>value</code> - timezone's name (like 'Europe/Athens')
  25. * <code>region</code> - timezone's region (for 'Europe/Athens' it will be 'Europe')
  26. * <code>city</code> - timezone's city (for 'Europe/Athens' it will be 'Athens')
  27. *
  28. * @typedef {{utcOffset: number, formattedOffset: string, value: string, region: string, city: string}} formattedTimezone
  29. */
  30. /**
  31. * List of timezones used in the user's settings popup
  32. *
  33. * <code>utcOffset</code> - offset-value (0, 180, 240 etc)
  34. * <code>value</code> - string like '120180|Europe'
  35. * <code>label</code> - string like '(UTC+02:00) Europe / Athens, Kiev, Minsk'
  36. * <code>zones</code> - list of zone-objects from <code>moment.tz</code> included to the <code>value</code>
  37. *
  38. * @typedef {{utcOffset: number, label: string, value: string, label: string, zones: object[]}} shownTimezone
  39. */
  40. module.exports = Em.Object.create({
  41. /**
  42. * @type {shownTimezone[]}
  43. * @readOnly
  44. */
  45. timezones: [],
  46. /**
  47. * Map of <code>timezones</code>
  48. * Key - timezone value (like '(UTC+01:00) Region / City1, City2')
  49. * Value - zone-object
  50. *
  51. * @type {object}
  52. * @readOnly
  53. */
  54. timezonesMappedByValue: function () {
  55. var ret = {};
  56. this.get('timezones').forEach(function (tz) {
  57. ret[tz.value] = tz;
  58. });
  59. return ret;
  60. }.property('timezones.[]'),
  61. init: function () {
  62. this.set('timezones', this._parseTimezones());
  63. return this._super();
  64. },
  65. /**
  66. * Load list of timezones from moment.tz
  67. * Zones "Etc/*" and abbreviations are excluded
  68. *
  69. * @returns {string[]}
  70. */
  71. getAllTimezoneNames: function () {
  72. return moment.tz.names().filter(function (timeZoneName) {
  73. return timeZoneName.indexOf('Etc/') !== 0 && timeZoneName !== timeZoneName.toUpperCase();
  74. });
  75. },
  76. /**
  77. * Try detect user's timezone using timezoneOffset and moment.tz
  78. * Checking current year January and July offsets
  79. * If <code>region</code> is provided, timezone for it is returned and not first valid
  80. *
  81. * @param {string} [region] preferred region (may be 'Europe', 'America', 'Africa', 'Asia' etc)
  82. * @returns {string}
  83. */
  84. detectUserTimezone: function (region) {
  85. region = (region || '').toLowerCase();
  86. var currentYear = new Date().getFullYear();
  87. var jan = new Date(currentYear, 0, 1);
  88. var jul = new Date(currentYear, 6, 1);
  89. var janOffset = jan.getTimezoneOffset();
  90. var julOffset = jul.getTimezoneOffset();
  91. var timezones = this.get('timezones');
  92. var validZones = [];
  93. for (var i = 0; i < timezones.length; i++) {
  94. var zones = timezones[i].zones;
  95. for (var j = 0; j < zones.length; j++) {
  96. var zone = moment.tz.zone(zones[j].value);
  97. if ((zone.offset(jan) === janOffset) && (zone.offset(jul) === julOffset)) {
  98. validZones.pushObject(timezones[i].value);
  99. }
  100. }
  101. }
  102. if (validZones.length) {
  103. if (region) {
  104. for (i = 0; i < validZones.length; i++) {
  105. if (validZones[i].toLowerCase().indexOf(region) !== -1) {
  106. return validZones[i];
  107. }
  108. }
  109. // Timezone for `region` wasn't found
  110. return validZones[0];
  111. }
  112. // `region` isn't provided, so return first valid timezone
  113. return validZones[0];
  114. }
  115. // are you from Venus?
  116. return '';
  117. },
  118. /**
  119. * Reformat timezones list and sort it by utcOffset and timeZoneName
  120. *
  121. * @private
  122. * @method _parseTimezones
  123. * @returns {shownTimezone[]}
  124. */
  125. _parseTimezones: function () {
  126. var currentYear = new Date().getFullYear();
  127. var jan = new Date(currentYear, 0, 1);
  128. var jul = new Date(currentYear, 6, 1);
  129. var zones = this.getAllTimezoneNames().map(function (timeZoneName) {
  130. var zone = moment(new Date()).tz(timeZoneName);
  131. var z = moment.tz.zone(timeZoneName);
  132. var offset = zone.format('Z');
  133. var regionCity = timeZoneName.split('/');
  134. var region = regionCity[0];
  135. var city = regionCity.length === 2 ? regionCity[1] : '';
  136. return {
  137. groupByKey: z.offset(jan) + '' + z.offset(jul),
  138. utcOffset: zone.utcOffset(),
  139. formattedOffset: offset,
  140. value: timeZoneName,
  141. region: region,
  142. city: city.replace(/_/g, ' ')
  143. };
  144. }).sort(function (zoneA, zoneB) {
  145. if (zoneA.utcOffset === zoneB.utcOffset) {
  146. if (zoneA.value === zoneB.value) {
  147. return 0;
  148. }
  149. return zoneA.value < zoneB.value ? -1 : 1;
  150. } else {
  151. if(zoneA.utcOffset === zoneB.utcOffset) {
  152. return 0;
  153. }
  154. return zoneA.utcOffset < zoneB.utcOffset ? -1 : 1;
  155. }
  156. });
  157. return this._groupTimezones(zones);
  158. },
  159. /**
  160. * Group timezones by <code>groupByKey</code>
  161. * Group timezones in the each group by <code>region</code>
  162. * <code>city</code> for each regions are joined into string 'city1, city2, city3' (empty cities and abbreviations are ignored)
  163. * Example:
  164. * <pre>
  165. * var zones = [
  166. * {groupByKey: '1', formattedOffset: '+01:00', value: 'a/Aa', region: 'a', city: 'Aa'},
  167. * {groupByKey: '1', formattedOffset: '+01:00', value: 'a/Bb', region: 'a', city: 'Bb'},
  168. * {groupByKey: '2', formattedOffset: '+02:00', value: 'a/Cc', region: 'a', city: 'Cc'},
  169. * {groupByKey: '2', formattedOffset: '+02:00', value: 'a/Dd', region: 'a', city: 'Dd'},
  170. * {groupByKey: '1', formattedOffset: '+01:00', value: 'b/Ee', region: 'b', city: 'Ee'},
  171. * {groupByKey: '1', formattedOffset: '+01:00', value: 'b/Ff', region: 'b', city: 'Ff'},
  172. * {groupByKey: '2', formattedOffset: '+02:00', value: 'b/Gg', region: 'b', city: 'Gg'},
  173. * {groupByKey: '2', formattedOffset: '+02:00', value: 'b/Hh', region: 'b', city: 'Hh'},
  174. * {groupByKey: '2', formattedOffset: '+02:00', value: 'b/II', region: 'b', city: 'II'}, // will be ignored, because city is abbreviation
  175. * {groupByKey: '2', formattedOffset: '+02:00', value: 'b', region: 'b', city: '' } // will be ignored, because city is empty
  176. * ];
  177. * var groupedZones = _groupTimezones(zones);
  178. * // groupedZones is:
  179. * [
  180. * {utcOffset: 1, label: '(UTC+01:00) a / Aa, Bb', value: '1|a'},
  181. * {utcOffset: 1, label: '(UTC+01:00) b / Ee, Ff', value: '1|b'},
  182. * {utcOffset: 2, label: '(UTC+02:00) a / Cc, Dd', value: '2|a'},
  183. * {utcOffset: 2, label: '(UTC+02:00) b / Gg, Hh', value: '2|b'}
  184. * ]
  185. * </pre>
  186. *
  187. * @param {formattedTimezone[]} zones
  188. * @returns {shownTimezone[]}
  189. * @method _groupTimezones
  190. * @private
  191. */
  192. _groupTimezones: function (zones) {
  193. var z = dataUtils.groupPropertyValues(zones, 'groupByKey');
  194. var newZones = [];
  195. Object.keys(z).forEach(function (offset) {
  196. var groupedByRegionZones = dataUtils.groupPropertyValues(z[offset], 'region');
  197. Object.keys(groupedByRegionZones).forEach(function (region) {
  198. var cities = groupedByRegionZones[region].mapProperty('city').filter(function (city) {
  199. return city !== '' && city !== city.toUpperCase();
  200. }).uniq().join(', ');
  201. var formattedOffset = Em.get(groupedByRegionZones[region], 'firstObject.formattedOffset');
  202. var utcOffset = Em.get(groupedByRegionZones[region], 'firstObject.utcOffset');
  203. var value = Em.get(groupedByRegionZones[region], 'firstObject.groupByKey') + '|' + region;
  204. var abbr = moment.tz(Em.get(groupedByRegionZones[region], 'firstObject.value')).format('z');
  205. newZones.pushObject({
  206. utcOffset: utcOffset,
  207. label: '(UTC' + formattedOffset + ' ' + abbr + ') ' + region + (cities ? ' / ' + cities : ''),
  208. value: value,
  209. zones: groupedByRegionZones[region]
  210. });
  211. });
  212. });
  213. return newZones.sortProperty('utcOffset');
  214. }
  215. });