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