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