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