You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LocaleDateFormat.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.hslf.util;
  16. import java.time.format.DateTimeFormatter;
  17. import java.time.format.DateTimeFormatterBuilder;
  18. import java.time.format.FormatStyle;
  19. import java.util.AbstractMap;
  20. import java.util.Locale;
  21. import java.util.Map;
  22. import java.util.function.Function;
  23. import java.util.stream.Collectors;
  24. import java.util.stream.Stream;
  25. import org.apache.poi.util.Internal;
  26. import org.apache.poi.util.LocaleID;
  27. import org.apache.poi.util.SuppressForbidden;
  28. @Internal
  29. public final class LocaleDateFormat {
  30. /**
  31. * Enum to specify initial remapping of the FormatID based on thd LCID
  32. */
  33. public enum MapFormatId {
  34. NONE, PPT
  35. }
  36. private enum MapFormatPPT {
  37. EN_US(LocaleID.EN_US, "MM/dd/yyyy", 1, 8, "MMMM dd, yyyy", 5, 9, 10, 11, 12, 15, 16, "h:mm a", "h:mm:ss a"),
  38. EN_AU(LocaleID.EN_AU, 0, 1, "d MMMM, yyy", 2, 5, 9, 10, 11, 12, 15, 16, 13, 14),
  39. JA_JP(LocaleID.JA_JP, 4, 8, 7, 3, 0, 9, 5, 11, 12, "HH:mm", "HH:mm:ss", 15, 16),
  40. ZH_TW(LocaleID.ZH_TW, 0, 1, 3, 7, 12, 9, 10, 4, 11, "HH:mm", "HH:mm:ss", "H:mm a", "H:mm:ss a"),
  41. KO_KR(LocaleID.KO_KR, 0, 1, 6, 3, 4, 10, 7, 12, 11, "HH:mm", "HH:mm:ss", 13, 14 ),
  42. AR_SA(LocaleID.AR_SA, 0, 1, 2, 3, 4, 5, 8, 7, 8, 1, 10, 11, 5),
  43. HE_IL(LocaleID.HE_IL, 0, 1, 2, 6, 11, 5, 12, 7, 8, 9, 1, 11, 6),
  44. SV_SE(LocaleID.SV_SE, 0, 1, 3, 2, 7, 9, 10, 11, 12, 15, 16, 13, 14),
  45. ZH_CN(LocaleID.ZH_CN, 0, 1, 2, 2, 4, 9, 5, "yyyy\u5E74M\u6708d\u65E5h\u65F6m\u5206", "yyyy\u5E74M\u6708d\u65E5\u661F\u671fWh\u65F6m\u5206s\u79D2", "HH:mm", "HH:mm:ss", "a h\u65F6m\u5206", "a h\u65F6m\u5206s\u79D2"),
  46. ZH_SG(LocaleID.ZH_SG, 0, 1, 3, 2, 4, 9, 5, "yyyy\u5E74M\u6708d\u65E5h\u65F6m\u5206", "yyyy\u5E74M\u6708d\u65E5\u661F\u671fWh\u65F6m\u5206s\u79D2", "HH:mm", "HH:mm:ss", "a h\u65F6m\u5206", "a h\u65F6m\u5206s\u79D2"),
  47. ZH_MO(LocaleID.ZH_MO, 0, 1, 3, 2, 4, 9, 5, "yyyy\u5E74M\u6708d\u65E5h\u65F6m\u5206", "yyyy\u5E74M\u6708d\u65E5\u661F\u671fWh\u65F6m\u5206s\u79D2", "HH:mm", "HH:mm:ss", "a h\u65F6m\u5206", "a h\u65F6m\u5206s\u79D2"),
  48. ZH_HK(LocaleID.ZH_HK, 0, 1, 3, 2, 4, 9, 5, "yyyy\u5E74M\u6708d\u65E5h\u65F6m\u5206", "yyyy\u5E74M\u6708d\u65E5\u661F\u671fWh\u65F6m\u5206s\u79D2", "HH:mm", "HH:mm:ss", "a h\u65F6m\u5206", "a h\u65F6m\u5206s\u79D2"),
  49. TH_TH(LocaleID.TH_TH, 0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14),
  50. VI_VN(LocaleID.VI_VN, 0, 1, 2, 3, 5, 6, 10, 11, 12, 13, 14, 15, 16),
  51. HI_IN(LocaleID.HI_IN, 1, 2, 3, 5, 7, 11, 13, 0, 1, 5, 10, 11, 14),
  52. SYR_SY(LocaleID.SYR_SY, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12),
  53. NO_MAP(LocaleID.INVALID_O, 0, 1, 3, 2, 5, 9, 10, 11, 12, 15, 16, 13, 14, 4, 6, 7, 8)
  54. ;
  55. private final LocaleID lcid;
  56. private final Object[] mapping;
  57. private static final Map<LocaleID,MapFormatPPT> LCID_LOOKUP =
  58. Stream.of(values()).collect(Collectors.toMap(MapFormatPPT::getLocaleID, Function.identity()));
  59. MapFormatPPT(LocaleID lcid, Object... mapping) {
  60. this.lcid = lcid;
  61. this.mapping = mapping;
  62. }
  63. public LocaleID getLocaleID() {
  64. return lcid;
  65. }
  66. public static Object mapFormatId(LocaleID lcid, int formatId) {
  67. Object[] mapping = LCID_LOOKUP.getOrDefault(lcid, NO_MAP).mapping;
  68. return (formatId >= 0 && formatId < mapping.length) ? mapping[formatId] : formatId;
  69. }
  70. }
  71. private enum MapFormatException {
  72. CHINESE(
  73. new LocaleID[]{LocaleID.ZH, LocaleID.ZH_HANS, LocaleID.ZH_HANT, LocaleID.ZH_CN, LocaleID.ZH_SG, LocaleID.ZH_MO, LocaleID.ZH_HK, LocaleID.ZH_YUE_HK},
  74. 0,
  75. 1,
  76. "yyyy\u5E74M\u6708d\u65E5\u661F\u671FW",
  77. "yyyy\u5E74M\u6708d\u65E5",
  78. "yyyy/M/d",
  79. "yy.M.d",
  80. "yyyy\u5E74M\u6708d\u65E5\u661F\u671FW",
  81. "yyyy\u5E74M\u6708d\u65E5",
  82. "yyyy\u5E74M\u6708d\u65E5\u661F\u671FW",
  83. "yyyy\u5E74M\u6708",
  84. "yyyy\u5E74M\u6708",
  85. "h\u65F6m\u5206s\u79D2",
  86. "h\u65F6m\u5206",
  87. "h\u65F6m\u5206",
  88. "h\u65F6m\u5206",
  89. "ah\u65F6m\u5206",
  90. "ah\u65F6m\u5206",
  91. // no lunar calendar support
  92. "EEEE\u5E74O\u6708A\u65E5",
  93. "EEEE\u5E74O\u6708A\u65E5\u661F\u671FW",
  94. "EEEE\u5E74O\u6708"
  95. ),
  96. // no hindu calendar support
  97. HINDI(
  98. new LocaleID[]{LocaleID.HI, LocaleID.HI_IN},
  99. "dd/M/g",
  100. "dddd, d MMMM yyyy",
  101. "dd MMMM yyyy",
  102. "dd/M/yy",
  103. "yy-M-dd",
  104. "d-MMMM-yyyy",
  105. "dd.M.g",
  106. "dd MMMM. yy",
  107. "dd MMMM yy",
  108. "MMMM YY",
  109. "MMMM-g",
  110. "dd/M/g HH:mm",
  111. "dd/M/g HH:mm:ss",
  112. "HH:mm a",
  113. "HH:mm:ss a",
  114. "HH:mm",
  115. "HH:mm:ss"
  116. ),
  117. // https://www.secondsite8.com/customdateformats.htm
  118. // aa or gg or o, r, i, c -> lunar calendar not supported
  119. // (aaa) -> lower case week names ... not supported
  120. JAPANESE(
  121. new LocaleID[]{LocaleID.JA, LocaleID.JA_JP, LocaleID.JA_PLOC_JP},
  122. 0,
  123. 1,
  124. "EEEy\u5E74M\u6708d\u65E5",
  125. "yyyy\u5E74M\u6708d\u65E5",
  126. "yyyy/M/d",
  127. "yyyy\u5E74M\u6708d\u65E5",
  128. "yy\u5E74M\u6708d\u65E5",
  129. "yyyy\u5E74M\u6708d\u65E5",
  130. "yyyy\u5E74M\u6708d\u65E5(EEE)",
  131. "yyyy\u5E74M\u6708",
  132. "yyyy\u5E74M\u6708",
  133. "yy/M/d H\u6642m\u5206",
  134. "yy/M/d H\u6642m\u5206s\u79D2",
  135. "a h\u6642m\u5206",
  136. "a h\u6642m\u5206s\u79D2",
  137. "H\u6642m\u5206",
  138. "H\u6642m\u5206s\u79D2",
  139. "yyyy\u5E74M\u6708d\u65E5 EEE\u66DC\u65E5"
  140. ),
  141. KOREAN(
  142. new LocaleID[]{LocaleID.KO,LocaleID.KO_KR},
  143. 0,
  144. 1,
  145. "yyyy\uB144 M\uC6D4 d\uC77C EEE\uC694\uC77C",
  146. "yyyy\uB144 M\uC6D4 d\uC77C",
  147. "yyyy/M/d",
  148. "yyMMdd",
  149. "yyyy\uB144 M\uC6D4 d\uC77C",
  150. "yyyy\uB144 M\uC6D4",
  151. "yyyy\uB144 M\uC6D4 d\uC77C",
  152. "yyyy",
  153. "yyyy\uB144 M\uC6D4",
  154. "yyyy\uB144 M\uC6D4 d\uC77C a h\uC2DC m\uBD84",
  155. "yy\uB144 M\uC6D4 d\uC77C H\uC2DC m\uBD84 s\uCD08",
  156. "a h\uC2DC m\uBD84",
  157. "a h\uC2DC m\uBD84 s\uCD08",
  158. "H\uC2DC m\uBD84",
  159. "H\uC2DC m\uBD84 S\uCD08"
  160. ),
  161. HUNGARIAN(
  162. new LocaleID[]{LocaleID.HU, LocaleID.HU_HU},
  163. 0, 1, 2, 3, 4, 5, 6, "yy. MMM. dd.", "\u2019yy MMM.", "MMMM \u2019yy", 10, 11, 12, "a h:mm", "a h:mm:ss", 15, 16
  164. ),
  165. BOKMAL(
  166. new LocaleID[]{LocaleID.NB_NO},
  167. 0, 1, 2, 3, 4, "d. MMM. yyyy", "d/m yyyy", "MMM. yy", "yyyy.mm.dd", 9, "d. MMM.", 11, 12, 13, 14, 15, 16
  168. ),
  169. CZECH(new LocaleID[]{LocaleID.CS, LocaleID.CS_CZ}, 0, 1, 2, 3, 4, 5, 6, 7, 8, "MMMM \u2019yy", 10, 11, 12, 13, 14, 15, 16),
  170. DANISH(new LocaleID[]{LocaleID.DA, LocaleID.DA_DK}, 0, "d. MMMM yyyy", "yy-MM-dd", "yyyy.MM.dd", 4, "MMMM yyyy", "d.M.yy", "d/M yyyy", "dd.MM.yyyy", "d.M.yyyy", "dd/MM yyyy", 11, 12, 13, 14, 15, 16 ),
  171. DUTCH(new LocaleID[]{LocaleID.NL,LocaleID.NL_BE,LocaleID.NL_NL}, 0, 1, 2, 3, 4, 5, 6, 7, 8, "MMMM \u2019yy", 10, 11, 12, 13, 14, 15, 16),
  172. FINISH(new LocaleID[]{LocaleID.FI, LocaleID.FI_FI}, 0, 1, 2, 3, 4, 5, 6, 7, 8, "MMMM \u2019yy", 10, 11, 12, 13, 14, 15, 16),
  173. FRENCH_CANADIAN(new LocaleID[]{LocaleID.FR_CA}, 0, 1, 2, "yy MM dd", 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16),
  174. GERMAN(new LocaleID[]{LocaleID.DE,LocaleID.DE_AT,LocaleID.DE_CH,LocaleID.DE_DE,LocaleID.DE_LI,LocaleID.DE_LU}, 0, 1, 2, 3, 4, "yy-MM-dd", 6, "dd. MMM. yyyy", 8, 9, 10, 11, 12, 13, 14, 15, 16),
  175. ITALIAN(new LocaleID[]{LocaleID.IT,LocaleID.IT_IT,LocaleID.IT_CH}, 0, 1, 2, 3, 4, "d-MMM.-yy", 6, "d. MMM. yy", "MMM. \u2019yy", "MMMM \u2019yy", 10, 11, 12, 13, 14, 15, 16),
  176. NO_MAP(new LocaleID[]{LocaleID.INVALID_O}, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
  177. // TODO: add others from [MS-OSHARED] chapter 2.4.4.4
  178. ;
  179. private final LocaleID[] lcid;
  180. private final Object[] mapping;
  181. private static final Map<LocaleID, MapFormatException> LCID_LOOKUP =
  182. Stream.of(values()).flatMap(m -> Stream.of(m.lcid).map(l -> new AbstractMap.SimpleEntry<>(l, m)))
  183. .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
  184. MapFormatException(LocaleID[] lcid, Object... mapping) {
  185. this.lcid = lcid;
  186. this.mapping = mapping;
  187. }
  188. public static Object mapFormatId(LocaleID lcid, int formatId) {
  189. Object[] mapping = LCID_LOOKUP.getOrDefault(lcid, NO_MAP).mapping;
  190. return (formatId >= 0 && formatId < mapping.length) ? mapping[formatId] : formatId;
  191. }
  192. }
  193. /**
  194. * This enum lists and describes the format indices that can be used as inputs to the algorithm. The
  195. * descriptions given are generalized; the actual format produced can vary from the description,
  196. * depending on the input locale.
  197. */
  198. @SuppressForbidden("DateTimeFormatter::ofLocalizedDate and others will be localized in mapFormatId")
  199. private enum MapFormatBase {
  200. /** 0 - Base short date **/
  201. SHORT_DATE(null, FormatStyle.MEDIUM, DateTimeFormatter::ofLocalizedDate),
  202. /** 1 - Base long date. **/
  203. LONG_DATE(null, FormatStyle.FULL, DateTimeFormatter::ofLocalizedDate),
  204. /**
  205. * 2 - Do the following to base long date:
  206. * - Remove occurrences of "dddd".
  207. * - Remove the comma symbol (0x002C) and space following "dddd" if present.
  208. * - Change occurrences of "dd" to "d".
  209. **/
  210. LONG_DATE_WITHOUT_WEEKDAY("d. MMMM yyyy", null, null),
  211. /**
  212. * 3 - Do the following to base short date:
  213. * - Change occurrences of "yyyy" to "yy".
  214. * - Change occurrences of "yy" to "yyyy".
  215. */
  216. ALTERNATE_SHORT_DATE("dd/MM/yy", null, null),
  217. /**
  218. * 4 - yyyy-MM-dd
  219. */
  220. ISO_STANDARD_DATE("yyyy-MM-dd", null, null),
  221. /**
  222. * 5 - If the symbol "y" occurs before the symbol "M" occurs in the base short date, the format is
  223. * "yy-MMM-d". Otherwise, the format is "d-MMM-yy".
  224. */
  225. SHORT_DATE_WITH_ABBREVIATED_MONTH("d-MMM-yy", null, null),
  226. /**
  227. * 6 - If the forward slash symbol (0x002F) occurs in the base short date, the slash symbol is the
  228. * period symbol (0x002E). Otherwise, the slash symbol is the forward slash (0x002F).
  229. * A group is an uninterrupted sequence of qualified symbols where a qualified symbol is "d",
  230. * "M", or "Y".
  231. * Identify the first three groups that occur in the base short date. The format is formed by
  232. * appending the three groups together with single slash symbols separating the groups.
  233. */
  234. SHORT_DATE_WITH_SLASHES("d/M/y", null, null),
  235. /**
  236. * 7 - Do the following to base long date:
  237. * - Remove occurrences of "dddd".
  238. * - Remove the comma symbol (0x002C) and space following "dddd" if present.
  239. * - Change occurrences of "dd" to "d".
  240. * - For all right-to-left locales and Lao, change a sequence of any length of "M" to "MMM".
  241. * - For all other locales, change a sequence of any length of "M" to "MMM".
  242. * - Change occurrences of "yyyy" to "yy".
  243. */
  244. ALTERNATE_SHORT_DATE_WITH_ABBREVIATED_MONTH("d. MMM yy", null, null),
  245. /**
  246. * 8 - For American English and Arabic, the format is "d MMMM yyyy".
  247. * For Hebrew, the format is "d MMMM, yyyy".
  248. * For all other locales, the format is the same as format 6 with the following additional step:
  249. * Change occurrences of "yyyy" to "yy".
  250. */
  251. ENGLISH_DATE("d MMMM yyyy", null, null),
  252. /**
  253. * 9 - Do the following to base long date:
  254. * - Remove all symbols that occur before the first occurrence of either the "y" symbol or the "M" symbol.
  255. * - Remove all "d" symbols.
  256. * - For all locales except Lithuanian, remove all period symbols (0x002E).
  257. * - Remove all comma symbols (0x002C).
  258. * - Change occurrences of "yyyy" to "yy".
  259. */
  260. MONTH_AND_YEAR("MMMM yy", null, null),
  261. /**
  262. * 10 - MMM-yy
  263. */
  264. ABBREVIATED_MONTH_AND_YEAR("LLL-yy", null, null),
  265. /**
  266. * 11 - Base short date followed by a space, followed by base time with seconds removed.
  267. * Seconds are removed by removing all "s" symbols and any symbol that directly precedes an
  268. * "s" symbol that is not an "h" or "m" symbol.
  269. */
  270. DATE_AND_HOUR12_TIME(null, FormatStyle.MEDIUM, (fs) -> new DateTimeFormatterBuilder().appendLocalized(FormatStyle.SHORT, null).appendLiteral(" ").appendLocalized(null, FormatStyle.SHORT).toFormatter()),
  271. /**
  272. * 12 - Base short date followed by a space, followed by base time.
  273. */
  274. DATE_AND_HOUR12_TIME_WITH_SECONDS(null, FormatStyle.MEDIUM, (fs) -> new DateTimeFormatterBuilder().appendLocalized(FormatStyle.SHORT, null).appendLiteral(" ").appendLocalized(null, fs).toFormatter()),
  275. /**
  276. * 13 - For Hungarian, the format is "am/pm h:mm".
  277. * For all other locales, the format is "h:mm am/pm".
  278. * In both cases, replace occurrences of the colon symbol (0x003A) with the time separator.
  279. */
  280. HOUR12_TIME("K:mm", null, null),
  281. /**
  282. * 14 - For Hungarian, the format is "am/pm h:mm:ss".
  283. * For all other locales, the format is "h:mm:ss am/pm".
  284. * In both cases, replace occurrences of the colon symbol (0x003A) with the time separator.
  285. */
  286. HOUR12_TIME_WITH_SECONDS("K:mm:ss", null, null),
  287. /**
  288. * 15 - "HH" followed by the time separator, followed by "mm".
  289. */
  290. HOUR24_TIME("HH:mm", null, null),
  291. /**
  292. * 16 - "HH" followed by the time separator, followed by "mm", followed by the time separator
  293. * followed by "ss".
  294. */
  295. HOUR24_TIME_WITH_SECONDS("HH:mm:ss", null, null),
  296. // CHINESE1(null, null, null),
  297. // CHINESE2(null, null, null),
  298. // CHINESE3(null, null, null)
  299. ;
  300. private final String datefmt;
  301. private final FormatStyle formatStyle;
  302. private final Function<FormatStyle,DateTimeFormatter> formatFct;
  303. MapFormatBase(String datefmt, FormatStyle formatStyle, Function<FormatStyle,DateTimeFormatter> formatFct) {
  304. this.formatStyle = formatStyle;
  305. this.datefmt = datefmt;
  306. this.formatFct = formatFct;
  307. }
  308. public static DateTimeFormatter mapFormatId(Locale loc, int formatId) {
  309. MapFormatBase[] mfb = MapFormatBase.values();
  310. if (formatId < 0 || formatId >= mfb.length) {
  311. return DateTimeFormatter.BASIC_ISO_DATE;
  312. }
  313. MapFormatBase mf = mfb[formatId];
  314. return (mf.datefmt == null)
  315. ? mf.formatFct.apply(mf.formatStyle).withLocale(loc)
  316. : DateTimeFormatter.ofPattern(mf.datefmt, loc);
  317. }
  318. }
  319. private LocaleDateFormat() {}
  320. public static DateTimeFormatter map(LocaleID lcid, int formatID, MapFormatId mapFormatId) {
  321. final Locale loc = Locale.forLanguageTag(lcid.getLanguageTag());
  322. int mappedFormatId = formatID;
  323. if (mapFormatId == MapFormatId.PPT) {
  324. Object mappedFormat = MapFormatPPT.mapFormatId(lcid, formatID);
  325. if (mappedFormat instanceof String) {
  326. return DateTimeFormatter.ofPattern((String)mappedFormat,loc);
  327. } else {
  328. mappedFormatId = (Integer)mappedFormat;
  329. }
  330. }
  331. Object mappedFormat = MapFormatException.mapFormatId(lcid, mappedFormatId);
  332. if (mappedFormat instanceof String) {
  333. return DateTimeFormatter.ofPattern((String)mappedFormat,loc);
  334. } else {
  335. return MapFormatBase.mapFormatId(loc, (Integer)mappedFormat);
  336. }
  337. }
  338. }