Home | History | Annotate | Download | only in icu
      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