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 17 package libcore.icu; 18 19 import java.util.Calendar; 20 import java.util.Locale; 21 import java.util.TimeZone; 22 import libcore.util.BasicLruCache; 23 24 /** 25 * Exposes icu4c's DateIntervalFormat. 26 */ 27 public 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 || dayDistance(startCalendar, endCalendar) <= 1)) { 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 dayDistance(Calendar c1, Calendar c2) { 228 return julianDay(c2) - julianDay(c1); 229 } 230 231 private static int julianDay(Calendar c) { 232 long utcMs = c.getTimeInMillis() + c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET); 233 return (int) (utcMs / DAY_IN_MS) + EPOCH_JULIAN_DAY; 234 } 235 236 private static native long createDateIntervalFormat(String skeleton, String localeName, String tzName); 237 private static native void destroyDateIntervalFormat(long address); 238 private static native String formatDateInterval(long address, long fromDate, long toDate); 239 } 240