| // © 2016 and later: Unicode, Inc. and others. |
| // License & terms of use: http://www.unicode.org/copyright.html |
| /* |
| ******************************************************************************* |
| * Copyright (C) 2007-2016, International Business Machines Corporation and |
| * others. All Rights Reserved. |
| ******************************************************************************* |
| */ |
| package com.ibm.icu.impl; |
| |
| import java.text.FieldPosition; |
| import java.text.ParsePosition; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.MissingResourceException; |
| |
| import com.ibm.icu.lang.UCharacter; |
| import com.ibm.icu.text.BreakIterator; |
| import com.ibm.icu.text.DateFormat; |
| import com.ibm.icu.text.DisplayContext; |
| import com.ibm.icu.text.MessageFormat; |
| import com.ibm.icu.text.SimpleDateFormat; |
| import com.ibm.icu.util.Calendar; |
| import com.ibm.icu.util.TimeZone; |
| import com.ibm.icu.util.ULocale; |
| import com.ibm.icu.util.UResourceBundle; |
| |
| /** |
| * @author srl |
| */ |
| public class RelativeDateFormat extends DateFormat { |
| |
| /** |
| * @author srl |
| * |
| */ |
| public static class URelativeString { |
| URelativeString(int offset, String string) { |
| this.offset = offset; |
| this.string = string; |
| } |
| URelativeString(String offset, String string) { |
| this.offset = Integer.parseInt(offset); |
| this.string = string; |
| } |
| public int offset; |
| public String string; |
| } |
| |
| // copy c'tor? |
| |
| /** |
| * @param timeStyle The time style for the date and time. |
| * @param dateStyle The date style for the date and time. |
| * @param locale The locale for the date. |
| * @param cal The calendar to be used |
| */ |
| public RelativeDateFormat(int timeStyle, int dateStyle, ULocale locale, Calendar cal) { |
| calendar = cal; |
| |
| fLocale = locale; |
| fTimeStyle = timeStyle; |
| fDateStyle = dateStyle; |
| |
| if (fDateStyle != DateFormat.NONE) { |
| int newStyle = fDateStyle & ~DateFormat.RELATIVE; |
| DateFormat df = DateFormat.getDateInstance(newStyle, locale); |
| if (df instanceof SimpleDateFormat) { |
| fDateTimeFormat = (SimpleDateFormat)df; |
| } else { |
| throw new IllegalArgumentException("Can't create SimpleDateFormat for date style"); |
| } |
| fDatePattern = fDateTimeFormat.toPattern(); |
| if (fTimeStyle != DateFormat.NONE) { |
| newStyle = fTimeStyle & ~DateFormat.RELATIVE; |
| df = DateFormat.getTimeInstance(newStyle, locale); |
| if (df instanceof SimpleDateFormat) { |
| fTimePattern = ((SimpleDateFormat)df).toPattern(); |
| } |
| } |
| } else { |
| // does not matter whether timeStyle is UDAT_NONE, we need something for fDateTimeFormat |
| int newStyle = fTimeStyle & ~DateFormat.RELATIVE; |
| DateFormat df = DateFormat.getTimeInstance(newStyle, locale); |
| if (df instanceof SimpleDateFormat) { |
| fDateTimeFormat = (SimpleDateFormat)df; |
| } else { |
| throw new IllegalArgumentException("Can't create SimpleDateFormat for time style"); |
| } |
| fTimePattern = fDateTimeFormat.toPattern(); |
| } |
| |
| initializeCalendar(null, fLocale); |
| loadDates(); |
| initializeCombinedFormat(calendar, fLocale); |
| } |
| |
| /** |
| * serial version (generated) |
| */ |
| private static final long serialVersionUID = 1131984966440549435L; |
| |
| /* (non-Javadoc) |
| * @see com.ibm.icu.text.DateFormat#format(com.ibm.icu.util.Calendar, java.lang.StringBuffer, java.text.FieldPosition) |
| */ |
| @Override |
| public StringBuffer format(Calendar cal, StringBuffer toAppendTo, |
| FieldPosition fieldPosition) { |
| |
| String relativeDayString = null; |
| DisplayContext capitalizationContext = getContext(DisplayContext.Type.CAPITALIZATION); |
| |
| if (fDateStyle != DateFormat.NONE) { |
| // calculate the difference, in days, between 'cal' and now. |
| int dayDiff = dayDifference(cal); |
| |
| // look up string |
| relativeDayString = getStringForDay(dayDiff); |
| } |
| |
| if (fDateTimeFormat != null) { |
| if (relativeDayString != null && fDatePattern != null && |
| (fTimePattern == null || fCombinedFormat == null || combinedFormatHasDateAtStart) ) { |
| // capitalize relativeDayString according to context for relative, set formatter no context |
| if ( relativeDayString.length() > 0 && UCharacter.isLowerCase(relativeDayString.codePointAt(0)) && |
| (capitalizationContext == DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE || |
| (capitalizationContext == DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU && capitalizationOfRelativeUnitsForListOrMenu) || |
| (capitalizationContext == DisplayContext.CAPITALIZATION_FOR_STANDALONE && capitalizationOfRelativeUnitsForStandAlone) )) { |
| if (capitalizationBrkIter == null) { |
| // should only happen when deserializing, etc. |
| capitalizationBrkIter = BreakIterator.getSentenceInstance(fLocale); |
| } |
| relativeDayString = UCharacter.toTitleCase(fLocale, relativeDayString, capitalizationBrkIter, |
| UCharacter.TITLECASE_NO_LOWERCASE | UCharacter.TITLECASE_NO_BREAK_ADJUSTMENT); |
| } |
| fDateTimeFormat.setContext(DisplayContext.CAPITALIZATION_NONE); |
| } else { |
| // set our context for the formatter |
| fDateTimeFormat.setContext(capitalizationContext); |
| } |
| } |
| |
| if (fDateTimeFormat != null && (fDatePattern != null || fTimePattern != null)) { |
| // The new way |
| if (fDatePattern == null) { |
| // must have fTimePattern |
| fDateTimeFormat.applyPattern(fTimePattern); |
| fDateTimeFormat.format(cal, toAppendTo, fieldPosition); |
| } else if (fTimePattern == null) { |
| // must have fDatePattern |
| if (relativeDayString != null) { |
| toAppendTo.append(relativeDayString); |
| } else { |
| fDateTimeFormat.applyPattern(fDatePattern); |
| fDateTimeFormat.format(cal, toAppendTo, fieldPosition); |
| } |
| } else { |
| String datePattern = fDatePattern; // default; |
| if (relativeDayString != null) { |
| // Need to quote the relativeDayString to make it a legal date pattern |
| datePattern = "'" + relativeDayString.replace("'", "''") + "'"; |
| } |
| StringBuffer combinedPattern = new StringBuffer(""); |
| fCombinedFormat.format(new Object[] {fTimePattern, datePattern}, combinedPattern, new FieldPosition(0)); |
| fDateTimeFormat.applyPattern(combinedPattern.toString()); |
| fDateTimeFormat.format(cal, toAppendTo, fieldPosition); |
| } |
| } else if (fDateFormat != null) { |
| // A subset of the old way, for serialization compatibility |
| // (just do the date part) |
| if (relativeDayString != null) { |
| toAppendTo.append(relativeDayString); |
| } else { |
| fDateFormat.format(cal, toAppendTo, fieldPosition); |
| } |
| } |
| |
| return toAppendTo; |
| } |
| |
| /* (non-Javadoc) |
| * @see com.ibm.icu.text.DateFormat#parse(java.lang.String, com.ibm.icu.util.Calendar, java.text.ParsePosition) |
| */ |
| @Override |
| public void parse(String text, Calendar cal, ParsePosition pos) { |
| throw new UnsupportedOperationException("Relative Date parse is not implemented yet"); |
| } |
| |
| /* (non-Javadoc) |
| * @see com.ibm.icu.text.DateFormat#setContext(com.ibm.icu.text.DisplayContext) |
| * Here we override the DateFormat implementation in order to |
| * lazily initialize relevant items |
| */ |
| @Override |
| public void setContext(DisplayContext context) { |
| super.setContext(context); |
| if (!capitalizationInfoIsSet && |
| (context==DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU || context==DisplayContext.CAPITALIZATION_FOR_STANDALONE)) { |
| initCapitalizationContextInfo(fLocale); |
| capitalizationInfoIsSet = true; |
| } |
| if (capitalizationBrkIter == null && (context==DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE || |
| (context==DisplayContext.CAPITALIZATION_FOR_UI_LIST_OR_MENU && capitalizationOfRelativeUnitsForListOrMenu) || |
| (context==DisplayContext.CAPITALIZATION_FOR_STANDALONE && capitalizationOfRelativeUnitsForStandAlone) )) { |
| capitalizationBrkIter = BreakIterator.getSentenceInstance(fLocale); |
| } |
| } |
| |
| private DateFormat fDateFormat; // keep for serialization compatibility |
| @SuppressWarnings("unused") |
| private DateFormat fTimeFormat; // now unused, keep for serialization compatibility |
| private MessageFormat fCombinedFormat; // the {0} {1} format. |
| private SimpleDateFormat fDateTimeFormat = null; // the held date/time formatter |
| private String fDatePattern = null; |
| private String fTimePattern = null; |
| |
| int fDateStyle; |
| int fTimeStyle; |
| ULocale fLocale; |
| |
| private transient List<URelativeString> fDates = null; |
| |
| private boolean combinedFormatHasDateAtStart = false; |
| private boolean capitalizationInfoIsSet = false; |
| private boolean capitalizationOfRelativeUnitsForListOrMenu = false; |
| private boolean capitalizationOfRelativeUnitsForStandAlone = false; |
| private transient BreakIterator capitalizationBrkIter = null; |
| |
| /** |
| * Get the string at a specific offset. |
| * @param day day offset ( -1, 0, 1, etc.. ). Does not require sorting by offset. |
| * @return the string, or NULL if none at that location. |
| */ |
| private String getStringForDay(int day) { |
| if(fDates == null) { |
| loadDates(); |
| } |
| for(URelativeString dayItem : fDates) { |
| if(dayItem.offset == day) { |
| return dayItem.string; |
| } |
| } |
| return null; |
| } |
| |
| // Sink to get "fields/day/relative". |
| private final class RelDateFmtDataSink extends UResource.Sink { |
| |
| @Override |
| public void put(UResource.Key key, UResource.Value value, boolean noFallback) { |
| if (value.getType() == ICUResourceBundle.ALIAS) { |
| return; |
| } |
| |
| UResource.Table table = value.getTable(); |
| for (int i = 0; table.getKeyAndValue(i, key, value); ++i) { |
| |
| int keyOffset; |
| try { |
| keyOffset = Integer.parseInt(key.toString()); |
| } |
| catch (NumberFormatException nfe) { |
| // Flag the error? |
| return; |
| } |
| // Check if already set. |
| if (getStringForDay(keyOffset) == null) { |
| URelativeString newDayInfo = new URelativeString(keyOffset, value.getString()); |
| fDates.add(newDayInfo); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Load the Date string array |
| */ |
| private synchronized void loadDates() { |
| ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, fLocale); |
| |
| // Use sink mechanism to traverse data structure. |
| fDates = new ArrayList<URelativeString>(); |
| RelDateFmtDataSink sink = new RelDateFmtDataSink(); |
| rb.getAllItemsWithFallback("fields/day/relative", sink); |
| } |
| |
| /** |
| * Set capitalizationOfRelativeUnitsForListOrMenu, capitalizationOfRelativeUnitsForStandAlone |
| */ |
| private void initCapitalizationContextInfo(ULocale locale) { |
| ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, locale); |
| try { |
| ICUResourceBundle rdb = rb.getWithFallback("contextTransforms/relative"); |
| int[] intVector = rdb.getIntVector(); |
| if (intVector.length >= 2) { |
| capitalizationOfRelativeUnitsForListOrMenu = (intVector[0] != 0); |
| capitalizationOfRelativeUnitsForStandAlone = (intVector[1] != 0); |
| } |
| } catch (MissingResourceException e) { |
| // use default |
| } |
| } |
| |
| /** |
| * @return the number of days in "until-now" |
| */ |
| private static int dayDifference(Calendar until) { |
| Calendar nowCal = (Calendar)until.clone(); |
| Date nowDate = new Date(System.currentTimeMillis()); |
| nowCal.clear(); |
| nowCal.setTime(nowDate); |
| int dayDiff = until.get(Calendar.JULIAN_DAY) - nowCal.get(Calendar.JULIAN_DAY); |
| return dayDiff; |
| } |
| |
| /** |
| * initializes fCalendar from parameters. Returns fCalendar as a convenience. |
| * @param zone Zone to be adopted, or NULL for TimeZone::createDefault(). |
| * @param locale Locale of the calendar |
| * @param status Error code |
| * @return the newly constructed fCalendar |
| */ |
| private Calendar initializeCalendar(TimeZone zone, ULocale locale) { |
| if (calendar == null) { |
| if(zone == null) { |
| calendar = Calendar.getInstance(locale); |
| } else { |
| calendar = Calendar.getInstance(zone, locale); |
| } |
| } |
| return calendar; |
| } |
| |
| private MessageFormat initializeCombinedFormat(Calendar cal, ULocale locale) { |
| String pattern; |
| ICUResourceBundle rb = (ICUResourceBundle) UResourceBundle.getBundleInstance( |
| ICUData.ICU_BASE_NAME, locale); |
| String resourcePath = "calendar/" + cal.getType() + "/DateTimePatterns"; |
| ICUResourceBundle patternsRb= rb.findWithFallback(resourcePath); |
| if (patternsRb == null && !cal.getType().equals("gregorian")) { |
| // Try again with gregorian, if not already attempted. |
| patternsRb = rb.findWithFallback("calendar/gregorian/DateTimePatterns"); |
| } |
| |
| if (patternsRb == null || patternsRb.getSize() < 9) { |
| // Undefined or too few elements. |
| pattern = "{1} {0}"; |
| } else { |
| int glueIndex = 8; |
| if (patternsRb.getSize() >= 13) { |
| if (fDateStyle >= DateFormat.FULL && fDateStyle <= DateFormat.SHORT) { |
| glueIndex += fDateStyle + 1; |
| } else |
| if (fDateStyle >= DateFormat.RELATIVE_FULL && |
| fDateStyle <= DateFormat.RELATIVE_SHORT) { |
| glueIndex += fDateStyle + 1 - DateFormat.RELATIVE; |
| } |
| } |
| int elementType = patternsRb.get(glueIndex).getType(); |
| if (elementType == UResourceBundle.ARRAY) { |
| pattern = patternsRb.get(glueIndex).getString(0); |
| } else { |
| pattern = patternsRb.getString(glueIndex); |
| } |
| } |
| combinedFormatHasDateAtStart = pattern.startsWith("{1}"); |
| fCombinedFormat = new MessageFormat(pattern, locale); |
| return fCombinedFormat; |
| } |
| } |