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

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