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.

DateTimeService.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. /*
  2. * Copyright 2000-2018 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.client;
  17. import java.util.Date;
  18. import java.util.logging.Level;
  19. import java.util.logging.Logger;
  20. import com.google.gwt.i18n.client.LocaleInfo;
  21. import com.google.gwt.i18n.client.TimeZone;
  22. import com.google.gwt.i18n.shared.DateTimeFormat;
  23. import com.vaadin.shared.ui.datefield.DateResolution;
  24. /**
  25. * This class provides date/time parsing services to all components on the
  26. * client side.
  27. *
  28. * @author Vaadin Ltd.
  29. *
  30. */
  31. @SuppressWarnings("deprecation")
  32. public class DateTimeService {
  33. private String locale;
  34. private static int[] maxDaysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30,
  35. 31, 30, 31 };
  36. private static final long MILLISECONDS_PER_DAY = 24 * 3600 * 1000;
  37. /**
  38. * Creates a new date time service with the application default locale.
  39. */
  40. public DateTimeService() {
  41. locale = LocaleService.getDefaultLocale();
  42. }
  43. /**
  44. * Creates a new date time service with a given locale.
  45. *
  46. * @param locale
  47. * e.g. {@code fi}, {@code en}, etc.
  48. * @throws LocaleNotLoadedException
  49. */
  50. public DateTimeService(String locale) throws LocaleNotLoadedException {
  51. setLocale(locale);
  52. }
  53. /**
  54. * Utility method to format positive int as zero-padded two-digits number.
  55. *
  56. * @param i
  57. * the value
  58. * @return "00".."99"
  59. * @since 8.4
  60. */
  61. public static String asTwoDigits(int i) {
  62. return (i < 10 ? "0" : "") + i;
  63. }
  64. public void setLocale(String locale) throws LocaleNotLoadedException {
  65. if (!LocaleService.getAvailableLocales().contains(locale)) {
  66. throw new LocaleNotLoadedException(locale);
  67. }
  68. this.locale = locale;
  69. }
  70. public String getLocale() {
  71. return locale;
  72. }
  73. public String getMonth(int month) {
  74. try {
  75. return LocaleService.getMonthNames(locale)[month];
  76. } catch (final LocaleNotLoadedException e) {
  77. getLogger().log(Level.SEVERE, "Error in getMonth", e);
  78. return null;
  79. }
  80. }
  81. public String getShortMonth(int month) {
  82. try {
  83. return LocaleService.getShortMonthNames(locale)[month];
  84. } catch (final LocaleNotLoadedException e) {
  85. getLogger().log(Level.SEVERE, "Error in getShortMonth", e);
  86. return null;
  87. }
  88. }
  89. public String getDay(int day) {
  90. try {
  91. return LocaleService.getDayNames(locale)[day];
  92. } catch (final LocaleNotLoadedException e) {
  93. getLogger().log(Level.SEVERE, "Error in getDay", e);
  94. return null;
  95. }
  96. }
  97. /**
  98. * Returns the localized short name of the specified day.
  99. *
  100. * @param day
  101. * the day, {@code 0} is {@code SUNDAY}
  102. * @return the localized short name
  103. */
  104. public String getShortDay(int day) {
  105. try {
  106. return LocaleService.getShortDayNames(locale)[day];
  107. } catch (final LocaleNotLoadedException e) {
  108. getLogger().log(Level.SEVERE, "Error in getShortDay", e);
  109. return null;
  110. }
  111. }
  112. /**
  113. * Returns the first day of the week, according to the used Locale.
  114. *
  115. * @return the localized first day of the week, {@code 0} is {@code SUNDAY}
  116. */
  117. public int getFirstDayOfWeek() {
  118. try {
  119. return LocaleService.getFirstDayOfWeek(locale);
  120. } catch (final LocaleNotLoadedException e) {
  121. getLogger().log(Level.SEVERE, "Error in getFirstDayOfWeek", e);
  122. return 0;
  123. }
  124. }
  125. /**
  126. * Returns whether the locale has twelve hour, or twenty four hour clock.
  127. *
  128. * @return {@code true} if the locale has twelve hour clock, {@code false}
  129. * for twenty four clock
  130. */
  131. public boolean isTwelveHourClock() {
  132. try {
  133. return LocaleService.isTwelveHourClock(locale);
  134. } catch (final LocaleNotLoadedException e) {
  135. getLogger().log(Level.SEVERE, "Error in isTwelveHourClock", e);
  136. return false;
  137. }
  138. }
  139. public String getClockDelimeter() {
  140. try {
  141. return LocaleService.getClockDelimiter(locale);
  142. } catch (final LocaleNotLoadedException e) {
  143. getLogger().log(Level.SEVERE, "Error in getClockDelimiter", e);
  144. return ":";
  145. }
  146. }
  147. private static final String[] DEFAULT_AMPM_STRINGS = { "AM", "PM" };
  148. public String[] getAmPmStrings() {
  149. try {
  150. return LocaleService.getAmPmStrings(locale);
  151. } catch (final LocaleNotLoadedException e) {
  152. // TODO can this practically even happen? Should die instead?
  153. getLogger().log(Level.SEVERE,
  154. "Locale not loaded, using fallback : AM/PM", e);
  155. return DEFAULT_AMPM_STRINGS;
  156. }
  157. }
  158. /**
  159. * Returns the first day of week of the specified {@code month}.
  160. *
  161. * @param month
  162. * the month, not {@code null}
  163. * @return the first day of week,
  164. */
  165. public int getStartWeekDay(Date month) {
  166. final Date dateForFirstOfThisMonth = new Date(month.getYear(),
  167. month.getMonth(), 1);
  168. int firstDay;
  169. try {
  170. firstDay = LocaleService.getFirstDayOfWeek(locale);
  171. } catch (final LocaleNotLoadedException e) {
  172. getLogger().log(Level.SEVERE, "Locale not loaded, using fallback 0",
  173. e);
  174. firstDay = 0;
  175. }
  176. int start = dateForFirstOfThisMonth.getDay() - firstDay;
  177. if (start < 0) {
  178. start += 7;
  179. }
  180. return start;
  181. }
  182. public static void setMilliseconds(Date date, int ms) {
  183. date.setTime(date.getTime() / 1000 * 1000 + ms);
  184. }
  185. public static int getMilliseconds(Date date) {
  186. if (date == null) {
  187. return 0;
  188. }
  189. return (int) (date.getTime() - date.getTime() / 1000 * 1000);
  190. }
  191. public static int getNumberOfDaysInMonth(Date date) {
  192. final int month = date.getMonth();
  193. if (month == 1 && isLeapYear(date)) {
  194. return 29;
  195. }
  196. return maxDaysInMonth[month];
  197. }
  198. public static boolean isLeapYear(Date date) {
  199. // Instantiate the date for 1st March of that year
  200. final Date firstMarch = new Date(date.getYear(), 2, 1);
  201. // Go back 1 day
  202. final long firstMarchTime = firstMarch.getTime();
  203. final long lastDayTimeFeb = firstMarchTime - (24 * 60 * 60 * 1000); // NUM_MILLISECS_A_DAY
  204. // Instantiate new Date with this time
  205. final Date febLastDay = new Date(lastDayTimeFeb);
  206. // Check for date in this new instance
  207. return (29 == febLastDay.getDate()) ? true : false;
  208. }
  209. public static boolean isSameDay(Date d1, Date d2) {
  210. return (getDayInt(d1) == getDayInt(d2));
  211. }
  212. public static boolean isInRange(Date date, Date rangeStart, Date rangeEnd,
  213. DateResolution resolution) {
  214. Date s;
  215. Date e;
  216. if (rangeStart.after(rangeEnd)) {
  217. s = rangeEnd;
  218. e = rangeStart;
  219. } else {
  220. e = rangeEnd;
  221. s = rangeStart;
  222. }
  223. long start = s.getYear() * 10000000000l;
  224. long end = e.getYear() * 10000000000l;
  225. long target = date.getYear() * 10000000000l;
  226. if (resolution == DateResolution.YEAR) {
  227. return (start <= target && end >= target);
  228. }
  229. start += s.getMonth() * 100000000l;
  230. end += e.getMonth() * 100000000l;
  231. target += date.getMonth() * 100000000l;
  232. if (resolution == DateResolution.MONTH) {
  233. return (start <= target && end >= target);
  234. }
  235. start += s.getDate() * 1000000l;
  236. end += e.getDate() * 1000000l;
  237. target += date.getDate() * 1000000l;
  238. if (resolution == DateResolution.DAY) {
  239. return (start <= target && end >= target);
  240. }
  241. start += s.getHours() * 10000l;
  242. end += e.getHours() * 10000l;
  243. target += date.getHours() * 10000l;
  244. start += s.getMinutes() * 100l;
  245. end += e.getMinutes() * 100l;
  246. target += date.getMinutes() * 100l;
  247. start += s.getSeconds();
  248. end += e.getSeconds();
  249. target += date.getSeconds();
  250. return (start <= target && end >= target);
  251. }
  252. private static int getDayInt(Date date) {
  253. final int y = date.getYear();
  254. final int m = date.getMonth();
  255. final int d = date.getDate();
  256. return ((y + 1900) * 10000 + m * 100 + d) * 1000000000;
  257. }
  258. /**
  259. * Returns the ISO-8601 week number of the given date.
  260. *
  261. * @param date
  262. * The date for which the week number should be resolved
  263. * @return The ISO-8601 week number for {@literal date}
  264. */
  265. public static int getISOWeekNumber(Date date) {
  266. int dayOfWeek = date.getDay(); // 0 == sunday
  267. // ISO 8601 use weeks that start on monday so we use
  268. // mon=1,tue=2,...sun=7;
  269. if (dayOfWeek == 0) {
  270. dayOfWeek = 7;
  271. }
  272. // Find nearest thursday (defines the week in ISO 8601). The week number
  273. // for the nearest thursday is the same as for the target date.
  274. int nearestThursdayDiff = 4 - dayOfWeek; // 4 is thursday
  275. Date nearestThursday = new Date(
  276. date.getTime() + nearestThursdayDiff * MILLISECONDS_PER_DAY);
  277. Date firstOfJanuary = new Date(nearestThursday.getYear(), 0, 1);
  278. long timeDiff = nearestThursday.getTime() - firstOfJanuary.getTime();
  279. // Rounding the result, as the division doesn't result in an integer
  280. // when the given date is inside daylight saving time period.
  281. int daysSinceFirstOfJanuary = (int) Math
  282. .round((double) timeDiff / MILLISECONDS_PER_DAY);
  283. int weekNumber = (daysSinceFirstOfJanuary) / 7 + 1;
  284. return weekNumber;
  285. }
  286. /**
  287. * Check if format contains the month name. If it does we manually convert
  288. * it to the month name since DateTimeFormat.format always uses the current
  289. * locale and will replace the month name wrong if current locale is
  290. * different from the locale set for the DateField.
  291. *
  292. * MMMM is converted into long month name, MMM is converted into short month
  293. * name. '' are added around the name to avoid that DateTimeFormat parses
  294. * the month name as a pattern.
  295. *
  296. * @param date
  297. * The date to convert
  298. * @param formatStr
  299. * The format string that might contain MMM or MMMM
  300. * @return
  301. */
  302. public String formatDate(Date date, String formatStr) {
  303. return formatDate(date, formatStr, null);
  304. }
  305. /**
  306. * Check if format contains the month name. If it does we manually convert
  307. * it to the month name since DateTimeFormat.format always uses the current
  308. * locale and will replace the month name wrong if current locale is
  309. * different from the locale set for the DateField.
  310. *
  311. * MMMM is converted into long month name, MMM is converted into short month
  312. * name. '' are added around the name to avoid that DateTimeFormat parses
  313. * the month name as a pattern.
  314. *
  315. * z is converted into the time zone name, using the specified
  316. * {@code timeZoneJSON}
  317. *
  318. * @param date
  319. * The date to convert
  320. * @param formatStr
  321. * The format string that might contain {@code MMM} or
  322. * {@code MMMM}
  323. * @param timeZone
  324. * The {@link TimeZone} used to replace {@code z}, can be
  325. * {@code null}
  326. *
  327. * @return the formatted date string
  328. * @since 8.2
  329. */
  330. public String formatDate(Date date, String formatStr, TimeZone timeZone) {
  331. /*
  332. * Format month and day names separately when locale for the
  333. * DateTimeService is not the same as the browser locale
  334. */
  335. formatStr = formatTimeZone(date, formatStr, timeZone);
  336. formatStr = formatMonthNames(date, formatStr);
  337. formatStr = formatDayNames(date, formatStr);
  338. // Format uses the browser locale
  339. DateTimeFormat format = DateTimeFormat.getFormat(formatStr);
  340. String result = format.format(date);
  341. return result;
  342. }
  343. private String formatDayNames(Date date, String formatStr) {
  344. if (formatStr.contains("EEEE")) {
  345. String dayName = getDay(date.getDay());
  346. if (dayName != null) {
  347. /*
  348. * Replace 4 or more E:s with the quoted day name. Also
  349. * concatenate generated string with any other string prepending
  350. * or following the EEEE pattern, i.e. 'EEEE'ta ' becomes 'DAYta
  351. * ' and not 'DAY''ta ', 'ab'EEEE becomes 'abDAY', 'x'EEEE'y'
  352. * becomes 'xDAYy'.
  353. */
  354. formatStr = formatStr.replaceAll("'([E]{4,})'", dayName);
  355. formatStr = formatStr.replaceAll("([E]{4,})'", "'" + dayName);
  356. formatStr = formatStr.replaceAll("'([E]{4,})", dayName + "'");
  357. formatStr = formatStr.replaceAll("[E]{4,}",
  358. "'" + dayName + "'");
  359. }
  360. }
  361. if (formatStr.contains("EEE")) {
  362. String dayName = getShortDay(date.getDay());
  363. if (dayName != null) {
  364. /*
  365. * Replace 3 or more E:s with the quoted month name. Also
  366. * concatenate generated string with any other string prepending
  367. * or following the EEE pattern, i.e. 'EEE'ta ' becomes 'DAYta '
  368. * and not 'DAY''ta ', 'ab'EEE becomes 'abDAY', 'x'EEE'y'
  369. * becomes 'xDAYy'.
  370. */
  371. formatStr = formatStr.replaceAll("'([E]{3,})'", dayName);
  372. formatStr = formatStr.replaceAll("([E]{3,})'", "'" + dayName);
  373. formatStr = formatStr.replaceAll("'([E]{3,})", dayName + "'");
  374. formatStr = formatStr.replaceAll("[E]{3,}",
  375. "'" + dayName + "'");
  376. }
  377. }
  378. return formatStr;
  379. }
  380. private String formatMonthNames(Date date, String formatStr) {
  381. if (formatStr.contains("MMMM")) {
  382. String monthName = getMonth(date.getMonth());
  383. if (monthName != null) {
  384. /*
  385. * Replace 4 or more M:s with the quoted month name. Also
  386. * concatenate generated string with any other string prepending
  387. * or following the MMMM pattern, i.e. 'MMMM'ta ' becomes
  388. * 'MONTHta ' and not 'MONTH''ta ', 'ab'MMMM becomes 'abMONTH',
  389. * 'x'MMMM'y' becomes 'xMONTHy'.
  390. */
  391. formatStr = formatStr.replaceAll("'([M]{4,})'", monthName);
  392. formatStr = formatStr.replaceAll("([M]{4,})'", "'" + monthName);
  393. formatStr = formatStr.replaceAll("'([M]{4,})", monthName + "'");
  394. formatStr = formatStr.replaceAll("[M]{4,}",
  395. "'" + monthName + "'");
  396. }
  397. }
  398. if (formatStr.contains("MMM")) {
  399. String monthName = getShortMonth(date.getMonth());
  400. if (monthName != null) {
  401. /*
  402. * Replace 3 or more M:s with the quoted month name. Also
  403. * concatenate generated string with any other string prepending
  404. * or following the MMM pattern, i.e. 'MMM'ta ' becomes 'MONTHta
  405. * ' and not 'MONTH''ta ', 'ab'MMM becomes 'abMONTH', 'x'MMM'y'
  406. * becomes 'xMONTHy'.
  407. */
  408. formatStr = formatStr.replaceAll("'([M]{3,})'", monthName);
  409. formatStr = formatStr.replaceAll("([M]{3,})'", "'" + monthName);
  410. formatStr = formatStr.replaceAll("'([M]{3,})", monthName + "'");
  411. formatStr = formatStr.replaceAll("[M]{3,}",
  412. "'" + monthName + "'");
  413. }
  414. }
  415. return formatStr;
  416. }
  417. private String formatTimeZone(Date date, String formatStr,
  418. TimeZone timeZone) {
  419. // if 'z' is found outside quotes and timeZone is used
  420. if (getIndexOf(formatStr, 'z') != -1 && timeZone != null) {
  421. return replaceTimeZone(formatStr, timeZone.getShortName(date));
  422. }
  423. return formatStr;
  424. }
  425. /**
  426. * Replaces the {@code z} characters of the specified {@code formatStr} with
  427. * the given {@code timeZoneName}.
  428. *
  429. * @param formatStr
  430. * The format string, which is the pattern describing the date
  431. * and time format
  432. * @param timeZoneName
  433. * the time zone name
  434. * @return the format string, with {@code z} replaced (if found)
  435. */
  436. private static String replaceTimeZone(String formatStr,
  437. String timeZoneName) {
  438. // search for 'z' outside the quotes (inside quotes is escaped)
  439. int start = getIndexOf(formatStr, 'z');
  440. if (start == -1) {
  441. return formatStr;
  442. }
  443. // if there are multiple consecutive 'z', treat them as one
  444. int end = start;
  445. while (end + 1 < formatStr.length()
  446. && formatStr.charAt(end + 1) == 'z') {
  447. end++;
  448. }
  449. return formatStr.substring(0, start) + "'" + timeZoneName + "'"
  450. + formatStr.substring(end + 1);
  451. }
  452. /**
  453. * Returns the first index of the specified {@code ch}, which is outside the
  454. * quotes.
  455. */
  456. private static int getIndexOf(String str, char ch) {
  457. boolean inQuote = false;
  458. for (int i = 0; i < str.length(); i++) {
  459. char c = str.charAt(i);
  460. if (c == '\'') {
  461. if (i + 1 < str.length() && str.charAt(i + 1) == '\'') {
  462. i++;
  463. } else {
  464. inQuote ^= true;
  465. }
  466. } else if (c == ch && !inQuote) {
  467. return i;
  468. }
  469. }
  470. return -1;
  471. }
  472. /**
  473. * Replaces month names in the entered date with the name in the current
  474. * browser locale.
  475. *
  476. * @param enteredDate
  477. * Date string e.g. "5 May 2010"
  478. * @param formatString
  479. * Format string e.g. "d M yyyy"
  480. * @return The date string where the month names have been replaced by the
  481. * browser locale version
  482. */
  483. private String parseMonthName(String enteredDate, String formatString) {
  484. LocaleInfo browserLocale = LocaleInfo.getCurrentLocale();
  485. if (browserLocale.getLocaleName().equals(getLocale())) {
  486. // No conversion needs to be done when locales match
  487. return enteredDate;
  488. }
  489. String[] browserMonthNames = browserLocale.getDateTimeConstants()
  490. .months();
  491. String[] browserShortMonthNames = browserLocale.getDateTimeConstants()
  492. .shortMonths();
  493. if (formatString.contains("MMMM")) {
  494. // Full month name
  495. for (int i = 0; i < 12; i++) {
  496. enteredDate = enteredDate.replaceAll(getMonth(i),
  497. browserMonthNames[i]);
  498. }
  499. }
  500. if (formatString.contains("MMM")) {
  501. // Short month name
  502. for (int i = 0; i < 12; i++) {
  503. enteredDate = enteredDate.replaceAll(getShortMonth(i),
  504. browserShortMonthNames[i]);
  505. }
  506. }
  507. return enteredDate;
  508. }
  509. /**
  510. * Parses the given date string using the given format string and the locale
  511. * set in this DateTimeService instance.
  512. *
  513. * @param dateString
  514. * Date string e.g. "1 February 2010"
  515. * @param formatString
  516. * Format string e.g. "d MMMM yyyy"
  517. * @param lenient
  518. * true to use lenient parsing, false to use strict parsing
  519. * @return A Date object representing the dateString. Never returns null.
  520. * @throws IllegalArgumentException
  521. * if the parsing fails
  522. *
  523. */
  524. public Date parseDate(String dateString, String formatString,
  525. boolean lenient) throws IllegalArgumentException {
  526. /* DateTimeFormat uses the browser's locale */
  527. DateTimeFormat format = DateTimeFormat.getFormat(formatString);
  528. /*
  529. * Parse month names separately when locale for the DateTimeService is
  530. * not the same as the browser locale
  531. */
  532. dateString = parseMonthName(dateString, formatString);
  533. Date date;
  534. if (lenient) {
  535. date = format.parse(dateString);
  536. } else {
  537. date = format.parseStrict(dateString);
  538. }
  539. // Some version of Firefox sets the timestamp to 0 if parsing fails.
  540. if (date != null && date.getTime() == 0) {
  541. throw new IllegalArgumentException(
  542. "Parsing of '" + dateString + "' failed");
  543. }
  544. return date;
  545. }
  546. private static Logger getLogger() {
  547. return Logger.getLogger(DateTimeService.class.getName());
  548. }
  549. }