DateIntervalFormat.java revision fd1d5e92b2eaf785cb18aa295b7b846cfc5e29af
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 java.util.Calendar;
20import java.util.Locale;
21import java.util.TimeZone;
22import libcore.util.BasicLruCache;
23
24/**
25 * Exposes icu4c's DateIntervalFormat.
26 */
27public final class DateIntervalFormat {
28
29  // These are all public API in DateUtils. There are others, but they're either for use with
30  // other methods (like FORMAT_ABBREV_RELATIVE), don't internationalize (like FORMAT_CAP_AMPM),
31  // or have never been implemented anyway.
32  public static final int FORMAT_SHOW_TIME      = 0x00001;
33  public static final int FORMAT_SHOW_WEEKDAY   = 0x00002;
34  public static final int FORMAT_SHOW_YEAR      = 0x00004;
35  public static final int FORMAT_NO_YEAR        = 0x00008;
36  public static final int FORMAT_SHOW_DATE      = 0x00010;
37  public static final int FORMAT_NO_MONTH_DAY   = 0x00020;
38  public static final int FORMAT_12HOUR         = 0x00040;
39  public static final int FORMAT_24HOUR         = 0x00080;
40  public static final int FORMAT_UTC            = 0x02000;
41  public static final int FORMAT_ABBREV_TIME    = 0x04000;
42  public static final int FORMAT_ABBREV_WEEKDAY = 0x08000;
43  public static final int FORMAT_ABBREV_MONTH   = 0x10000;
44  public static final int FORMAT_NUMERIC_DATE   = 0x20000;
45  public static final int FORMAT_ABBREV_ALL     = 0x80000;
46
47  private static final int DAY_IN_MS = 24 * 60 * 60 * 1000;
48  private static final int EPOCH_JULIAN_DAY = 2440588;
49
50  private static final FormatterCache CACHED_FORMATTERS = new FormatterCache();
51
52  static class FormatterCache extends BasicLruCache<String, Long> {
53    FormatterCache() {
54      super(8);
55    }
56
57    protected void entryEvicted(String key, Long value) {
58      destroyDateIntervalFormat(value);
59    }
60  };
61
62  private DateIntervalFormat() {
63  }
64
65  // This is public DateUtils API in frameworks/base.
66  public static String formatDateRange(long startMs, long endMs, int flags, String olsonId) {
67    if ((flags & FORMAT_UTC) != 0) {
68      olsonId = "UTC";
69    }
70    TimeZone tz = (olsonId != null) ? TimeZone.getTimeZone(olsonId) : TimeZone.getDefault();
71    return formatDateRange(Locale.getDefault(), tz, startMs, endMs, flags);
72  }
73
74  // This is our slightly more sensible internal API. (A truly sane replacement would take a
75  // skeleton instead of int flags.)
76  public static String formatDateRange(Locale locale, TimeZone tz, long startMs, long endMs, int flags) {
77    Calendar startCalendar = Calendar.getInstance(tz);
78    startCalendar.setTimeInMillis(startMs);
79
80    Calendar endCalendar;
81    if (startMs == endMs) {
82      endCalendar = startCalendar;
83    } else {
84      endCalendar = Calendar.getInstance(tz);
85      endCalendar.setTimeInMillis(endMs);
86    }
87
88    boolean endsAtMidnight = isMidnight(endCalendar);
89
90    // If we're not showing the time or the start and end times are on the same day, and the
91    // end time is midnight, fudge the end date so we don't count the day that's about to start.
92    // This is not the behavior of icu4c's DateIntervalFormat, but it's the historical behavior
93    // of Android's DateUtils.formatDateRange.
94    if (startMs != endMs && endsAtMidnight &&
95        ((flags & FORMAT_SHOW_TIME) == 0 || julianDay(startCalendar) == julianDay(endCalendar))) {
96      endCalendar.roll(Calendar.DAY_OF_MONTH, false);
97      endMs -= DAY_IN_MS;
98    }
99
100    String skeleton = toSkeleton(startCalendar, endCalendar, flags);
101    synchronized (CACHED_FORMATTERS) {
102      return formatDateInterval(getFormatter(skeleton, locale.toString(), tz.getID()), startMs, endMs);
103    }
104  }
105
106  private static long getFormatter(String skeleton, String localeName, String tzName) {
107    String key = skeleton + "\t" + localeName + "\t" + tzName;
108    Long formatter = CACHED_FORMATTERS.get(key);
109    if (formatter != null) {
110      return formatter;
111    }
112    long address = createDateIntervalFormat(skeleton, localeName, tzName);
113    CACHED_FORMATTERS.put(key, address);
114    return address;
115  }
116
117  private static String toSkeleton(Calendar startCalendar, Calendar endCalendar, int flags) {
118    if ((flags & FORMAT_ABBREV_ALL) != 0) {
119      flags |= FORMAT_ABBREV_MONTH | FORMAT_ABBREV_TIME | FORMAT_ABBREV_WEEKDAY;
120    }
121
122    String monthPart = "MMMM";
123    if ((flags & FORMAT_NUMERIC_DATE) != 0) {
124      monthPart = "M";
125    } else if ((flags & FORMAT_ABBREV_MONTH) != 0) {
126      monthPart = "MMM";
127    }
128
129    String weekPart = "EEEE";
130    if ((flags & FORMAT_ABBREV_WEEKDAY) != 0) {
131      weekPart = "EEE";
132    }
133
134    String timePart = "j"; // "j" means choose 12 or 24 hour based on current locale.
135    if ((flags & FORMAT_24HOUR) != 0) {
136      timePart = "H";
137    } else if ((flags & FORMAT_12HOUR) != 0) {
138      timePart = "h";
139    }
140
141    // If we've not been asked to abbreviate times, or we're using the 24-hour clock (where it
142    // never makes sense to leave out the minutes), include minutes. This gets us times like
143    // "4 PM" while avoiding times like "16" (for "16:00").
144    if ((flags & FORMAT_ABBREV_TIME) == 0 || (flags & FORMAT_24HOUR) != 0) {
145      timePart += "m";
146    } else {
147      // Otherwise, we're abbreviating a 12-hour time, and should only show the minutes
148      // if they're not both "00".
149      if (!(onTheHour(startCalendar) && onTheHour(endCalendar))) {
150        timePart = timePart + "m";
151      }
152    }
153
154    if (fallOnDifferentDates(startCalendar, endCalendar)) {
155      flags |= FORMAT_SHOW_DATE;
156    }
157
158    if (fallInSameMonth(startCalendar, endCalendar) && (flags & FORMAT_NO_MONTH_DAY) != 0) {
159      flags &= (~FORMAT_SHOW_WEEKDAY);
160      flags &= (~FORMAT_SHOW_TIME);
161    }
162
163    if ((flags & (FORMAT_SHOW_DATE | FORMAT_SHOW_TIME | FORMAT_SHOW_WEEKDAY)) == 0) {
164      flags |= FORMAT_SHOW_DATE;
165    }
166
167    // If we've been asked to show the date, work out whether we think we should show the year.
168    if ((flags & FORMAT_SHOW_DATE) != 0) {
169      if ((flags & FORMAT_SHOW_YEAR) != 0) {
170        // The caller explicitly wants us to show the year.
171      } else if ((flags & FORMAT_NO_YEAR) != 0) {
172        // The caller explicitly doesn't want us to show the year, even if we otherwise would.
173      } else if (!fallInSameYear(startCalendar, endCalendar) || !isThisYear(startCalendar)) {
174        flags |= FORMAT_SHOW_YEAR;
175      }
176    }
177
178    StringBuilder builder = new StringBuilder();
179    if ((flags & (FORMAT_SHOW_DATE | FORMAT_NO_MONTH_DAY)) != 0) {
180      if ((flags & FORMAT_SHOW_YEAR) != 0) {
181        builder.append("y");
182      }
183      builder.append(monthPart);
184      if ((flags & FORMAT_NO_MONTH_DAY) == 0) {
185        builder.append("d");
186      }
187    }
188    if ((flags & FORMAT_SHOW_WEEKDAY) != 0) {
189      builder.append(weekPart);
190    }
191    if ((flags & FORMAT_SHOW_TIME) != 0) {
192      builder.append(timePart);
193    }
194    return builder.toString();
195  }
196
197  private static boolean isMidnight(Calendar c) {
198    return c.get(Calendar.HOUR_OF_DAY) == 0 &&
199        c.get(Calendar.MINUTE) == 0 &&
200        c.get(Calendar.SECOND) == 0 &&
201        c.get(Calendar.MILLISECOND) == 0;
202  }
203
204  private static boolean onTheHour(Calendar c) {
205    return c.get(Calendar.MINUTE) == 0 && c.get(Calendar.SECOND) == 0;
206  }
207
208  private static boolean fallOnDifferentDates(Calendar c1, Calendar c2) {
209    return c1.get(Calendar.YEAR) != c2.get(Calendar.YEAR) ||
210        c1.get(Calendar.MONTH) != c2.get(Calendar.MONTH) ||
211        c1.get(Calendar.DAY_OF_MONTH) != c2.get(Calendar.DAY_OF_MONTH);
212  }
213
214  private static boolean fallInSameMonth(Calendar c1, Calendar c2) {
215    return c1.get(Calendar.MONTH) == c2.get(Calendar.MONTH);
216  }
217
218  private static boolean fallInSameYear(Calendar c1, Calendar c2) {
219    return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR);
220  }
221
222  private static boolean isThisYear(Calendar c) {
223    Calendar now = Calendar.getInstance(c.getTimeZone());
224    return c.get(Calendar.YEAR) == now.get(Calendar.YEAR);
225  }
226
227  private static int julianDay(Calendar c) {
228    long utcMs = c.get(Calendar.MILLISECOND) + c.get(Calendar.ZONE_OFFSET);
229    return (int) (utcMs / DAY_IN_MS) + EPOCH_JULIAN_DAY;
230  }
231
232  private static native long createDateIntervalFormat(String skeleton, String localeName, String tzName);
233  private static native void destroyDateIntervalFormat(long address);
234  private static native String formatDateInterval(long address, long fromDate, long toDate);
235}
236