DateIntervalFormat.java revision 12fbbc16959d078dde23b3b15c8d3d127ef33507
1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package libcore.icu;
18
19import com.ibm.icu.impl.JavaTimeZone;
20import com.ibm.icu.util.Calendar;
21import com.ibm.icu.util.GregorianCalendar;
22import com.ibm.icu.util.ULocale;
23
24import java.text.FieldPosition;
25import java.util.TimeZone;
26import libcore.util.BasicLruCache;
27
28/**
29 * Exposes icu4j's DateIntervalFormat.
30 */
31public final class DateIntervalFormat {
32
33  // These are all public API in DateUtils. There are others, but they're either for use with
34  // other methods (like FORMAT_ABBREV_RELATIVE), don't internationalize (like FORMAT_CAP_AMPM),
35  // or have never been implemented anyway.
36  public static final int FORMAT_SHOW_TIME      = 0x00001;
37  public static final int FORMAT_SHOW_WEEKDAY   = 0x00002;
38  public static final int FORMAT_SHOW_YEAR      = 0x00004;
39  public static final int FORMAT_NO_YEAR        = 0x00008;
40  public static final int FORMAT_SHOW_DATE      = 0x00010;
41  public static final int FORMAT_NO_MONTH_DAY   = 0x00020;
42  public static final int FORMAT_12HOUR         = 0x00040;
43  public static final int FORMAT_24HOUR         = 0x00080;
44  public static final int FORMAT_UTC            = 0x02000;
45  public static final int FORMAT_ABBREV_TIME    = 0x04000;
46  public static final int FORMAT_ABBREV_WEEKDAY = 0x08000;
47  public static final int FORMAT_ABBREV_MONTH   = 0x10000;
48  public static final int FORMAT_NUMERIC_DATE   = 0x20000;
49  public static final int FORMAT_ABBREV_ALL     = 0x80000;
50
51  private static final FormatterCache CACHED_FORMATTERS = new FormatterCache();
52
53  static class FormatterCache extends BasicLruCache<String, com.ibm.icu.text.DateIntervalFormat> {
54    FormatterCache() {
55      super(8);
56    }
57  }
58
59  private DateIntervalFormat() {
60  }
61
62  // This is public DateUtils API in frameworks/base.
63  public static String formatDateRange(long startMs, long endMs, int flags, String olsonId) {
64    if ((flags & FORMAT_UTC) != 0) {
65      olsonId = "UTC";
66    }
67    // We create a java.util.TimeZone here to use libcore's data and libcore's olson ID / pseudo-tz
68    // logic.
69    TimeZone tz = (olsonId != null) ? TimeZone.getTimeZone(olsonId) : TimeZone.getDefault();
70    com.ibm.icu.util.TimeZone icuTimeZone = icuTimeZone(tz);
71    ULocale icuLocale = ULocale.getDefault();
72    return formatDateRange(icuLocale, icuTimeZone, startMs, endMs, flags);
73  }
74
75  // This is our slightly more sensible internal API. (A truly sane replacement would take a
76  // skeleton instead of int flags.)
77  public static String formatDateRange(ULocale icuLocale, com.ibm.icu.util.TimeZone icuTimeZone,
78      long startMs, long endMs, int flags) {
79    Calendar startCalendar = createIcuCalendar(icuTimeZone, icuLocale, startMs);
80    Calendar endCalendar;
81    if (startMs == endMs) {
82      endCalendar = startCalendar;
83    } else {
84      endCalendar = createIcuCalendar(icuTimeZone, icuLocale, endMs);
85    }
86
87    boolean endsAtMidnight = isMidnight(endCalendar);
88
89    // If we're not showing the time or the start and end times are on the same day, and the
90    // end time is midnight, fudge the end date so we don't count the day that's about to start.
91    // This is not the behavior of icu4j's DateIntervalFormat, but it's the historical behavior
92    // of Android's DateUtils.formatDateRange.
93    if (startMs != endMs && endsAtMidnight &&
94        ((flags & FORMAT_SHOW_TIME) == 0 || dayDistance(startCalendar, endCalendar) <= 1)) {
95      endCalendar.roll(Calendar.DAY_OF_MONTH, false);
96    }
97
98    String skeleton = toSkeleton(startCalendar, endCalendar, flags);
99    synchronized (CACHED_FORMATTERS) {
100      com.ibm.icu.text.DateIntervalFormat formatter =
101          getFormatter(skeleton, icuLocale, icuTimeZone);
102      return formatter.format(startCalendar, endCalendar, new StringBuffer(),
103          new FieldPosition(0)).toString();
104    }
105  }
106
107  private static com.ibm.icu.text.DateIntervalFormat getFormatter(String skeleton, ULocale locale,
108      com.ibm.icu.util.TimeZone icuTimeZone) {
109    String key = skeleton + "\t" + locale + "\t" + icuTimeZone;
110    com.ibm.icu.text.DateIntervalFormat formatter = CACHED_FORMATTERS.get(key);
111    if (formatter != null) {
112      return formatter;
113    }
114    formatter = com.ibm.icu.text.DateIntervalFormat.getInstance(skeleton, locale);
115    formatter.setTimeZone(icuTimeZone);
116    CACHED_FORMATTERS.put(key, formatter);
117    return formatter;
118  }
119
120  private static String toSkeleton(Calendar startCalendar, Calendar endCalendar, int flags) {
121    if ((flags & FORMAT_ABBREV_ALL) != 0) {
122      flags |= FORMAT_ABBREV_MONTH | FORMAT_ABBREV_TIME | FORMAT_ABBREV_WEEKDAY;
123    }
124
125    String monthPart = "MMMM";
126    if ((flags & FORMAT_NUMERIC_DATE) != 0) {
127      monthPart = "M";
128    } else if ((flags & FORMAT_ABBREV_MONTH) != 0) {
129      monthPart = "MMM";
130    }
131
132    String weekPart = "EEEE";
133    if ((flags & FORMAT_ABBREV_WEEKDAY) != 0) {
134      weekPart = "EEE";
135    }
136
137    String timePart = "j"; // "j" means choose 12 or 24 hour based on current locale.
138    if ((flags & FORMAT_24HOUR) != 0) {
139      timePart = "H";
140    } else if ((flags & FORMAT_12HOUR) != 0) {
141      timePart = "h";
142    }
143
144    // If we've not been asked to abbreviate times, or we're using the 24-hour clock (where it
145    // never makes sense to leave out the minutes), include minutes. This gets us times like
146    // "4 PM" while avoiding times like "16" (for "16:00").
147    if ((flags & FORMAT_ABBREV_TIME) == 0 || (flags & FORMAT_24HOUR) != 0) {
148      timePart += "m";
149    } else {
150      // Otherwise, we're abbreviating a 12-hour time, and should only show the minutes
151      // if they're not both "00".
152      if (!(onTheHour(startCalendar) && onTheHour(endCalendar))) {
153        timePart = timePart + "m";
154      }
155    }
156
157    if (fallOnDifferentDates(startCalendar, endCalendar)) {
158      flags |= FORMAT_SHOW_DATE;
159    }
160
161    if (fallInSameMonth(startCalendar, endCalendar) && (flags & FORMAT_NO_MONTH_DAY) != 0) {
162      flags &= (~FORMAT_SHOW_WEEKDAY);
163      flags &= (~FORMAT_SHOW_TIME);
164    }
165
166    if ((flags & (FORMAT_SHOW_DATE | FORMAT_SHOW_TIME | FORMAT_SHOW_WEEKDAY)) == 0) {
167      flags |= FORMAT_SHOW_DATE;
168    }
169
170    // If we've been asked to show the date, work out whether we think we should show the year.
171    if ((flags & FORMAT_SHOW_DATE) != 0) {
172      if ((flags & FORMAT_SHOW_YEAR) != 0) {
173        // The caller explicitly wants us to show the year.
174      } else if ((flags & FORMAT_NO_YEAR) != 0) {
175        // The caller explicitly doesn't want us to show the year, even if we otherwise would.
176      } else if (!fallInSameYear(startCalendar, endCalendar) || !isThisYear(startCalendar)) {
177        flags |= FORMAT_SHOW_YEAR;
178      }
179    }
180
181    StringBuilder builder = new StringBuilder();
182    if ((flags & (FORMAT_SHOW_DATE | FORMAT_NO_MONTH_DAY)) != 0) {
183      if ((flags & FORMAT_SHOW_YEAR) != 0) {
184        builder.append("y");
185      }
186      builder.append(monthPart);
187      if ((flags & FORMAT_NO_MONTH_DAY) == 0) {
188        builder.append("d");
189      }
190    }
191    if ((flags & FORMAT_SHOW_WEEKDAY) != 0) {
192      builder.append(weekPart);
193    }
194    if ((flags & FORMAT_SHOW_TIME) != 0) {
195      builder.append(timePart);
196    }
197    return builder.toString();
198  }
199
200  private static boolean isMidnight(Calendar c) {
201    return c.get(Calendar.HOUR_OF_DAY) == 0 &&
202        c.get(Calendar.MINUTE) == 0 &&
203        c.get(Calendar.SECOND) == 0 &&
204        c.get(Calendar.MILLISECOND) == 0;
205  }
206
207  private static boolean onTheHour(Calendar c) {
208    return c.get(Calendar.MINUTE) == 0 && c.get(Calendar.SECOND) == 0;
209  }
210
211  private static boolean fallOnDifferentDates(Calendar c1, Calendar c2) {
212    return c1.get(Calendar.YEAR) != c2.get(Calendar.YEAR) ||
213        c1.get(Calendar.MONTH) != c2.get(Calendar.MONTH) ||
214        c1.get(Calendar.DAY_OF_MONTH) != c2.get(Calendar.DAY_OF_MONTH);
215  }
216
217  private static boolean fallInSameMonth(Calendar c1, Calendar c2) {
218    return c1.get(Calendar.MONTH) == c2.get(Calendar.MONTH);
219  }
220
221  private static boolean fallInSameYear(Calendar c1, Calendar c2) {
222    return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR);
223  }
224
225  private static boolean isThisYear(Calendar c) {
226    Calendar now = (Calendar) c.clone();
227    now.setTimeInMillis(System.currentTimeMillis());
228    return c.get(Calendar.YEAR) == now.get(Calendar.YEAR);
229  }
230
231  public static int dayDistance(Calendar c1, Calendar c2) {
232    return c2.get(Calendar.JULIAN_DAY) - c1.get(Calendar.JULIAN_DAY);
233  }
234
235  /**
236   * Creates an immutable ICU timezone backed by the specified libcore timezone data. At the time of
237   * writing the libcore implementation is faster but restricted to 1902 - 2038.
238   * Callers must not modify the {@code tz} after calling this method.
239   */
240  static com.ibm.icu.util.TimeZone icuTimeZone(TimeZone tz) {
241    JavaTimeZone javaTimeZone = new JavaTimeZone(tz, null);
242    javaTimeZone.freeze(); // Optimization - allows the timezone to be copied cheaply.
243    return javaTimeZone;
244  }
245
246  static Calendar createIcuCalendar(com.ibm.icu.util.TimeZone icuTimeZone, ULocale icuLocale,
247      long timeInMillis) {
248    Calendar calendar = new GregorianCalendar(icuTimeZone, icuLocale);
249    calendar.setTimeInMillis(timeInMillis);
250    return calendar;
251  }
252}
253