Home | History | Annotate | Download | only in widget
      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 android.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.Configuration;
     21 import android.content.res.TypedArray;
     22 import android.os.Parcel;
     23 import android.os.Parcelable;
     24 import android.text.format.DateFormat;
     25 import android.text.format.DateUtils;
     26 import android.util.AttributeSet;
     27 import android.view.LayoutInflater;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.view.accessibility.AccessibilityEvent;
     31 import android.view.accessibility.AccessibilityNodeInfo;
     32 import android.view.inputmethod.EditorInfo;
     33 import android.view.inputmethod.InputMethodManager;
     34 import com.android.internal.R;
     35 
     36 import java.text.DateFormatSymbols;
     37 import java.util.Calendar;
     38 import java.util.Locale;
     39 
     40 import libcore.icu.LocaleData;
     41 
     42 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
     43 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
     44 
     45 /**
     46  * A delegate implementing the basic TimePicker
     47  */
     48 class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
     49     private static final boolean DEFAULT_ENABLED_STATE = true;
     50     private static final int HOURS_IN_HALF_DAY = 12;
     51 
     52     // state
     53     private boolean mIs24HourView;
     54     private boolean mIsAm;
     55 
     56     // ui components
     57     private final NumberPicker mHourSpinner;
     58     private final NumberPicker mMinuteSpinner;
     59     private final NumberPicker mAmPmSpinner;
     60     private final EditText mHourSpinnerInput;
     61     private final EditText mMinuteSpinnerInput;
     62     private final EditText mAmPmSpinnerInput;
     63     private final TextView mDivider;
     64 
     65     // Note that the legacy implementation of the TimePicker is
     66     // using a button for toggling between AM/PM while the new
     67     // version uses a NumberPicker spinner. Therefore the code
     68     // accommodates these two cases to be backwards compatible.
     69     private final Button mAmPmButton;
     70 
     71     private final String[] mAmPmStrings;
     72 
     73     private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
     74     private Calendar mTempCalendar;
     75     private boolean mHourWithTwoDigit;
     76     private char mHourFormat;
     77 
     78     public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs,
     79             int defStyleAttr, int defStyleRes) {
     80         super(delegator, context);
     81 
     82         // process style attributes
     83         final TypedArray a = mContext.obtainStyledAttributes(
     84                 attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
     85         final int layoutResourceId = a.getResourceId(
     86                 R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy);
     87         a.recycle();
     88 
     89         final LayoutInflater inflater = LayoutInflater.from(mContext);
     90         inflater.inflate(layoutResourceId, mDelegator, true);
     91 
     92         // hour
     93         mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour);
     94         mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
     95             public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
     96                 updateInputState();
     97                 if (!is24HourView()) {
     98                     if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) ||
     99                             (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
    100                         mIsAm = !mIsAm;
    101                         updateAmPmControl();
    102                     }
    103                 }
    104                 onTimeChanged();
    105             }
    106         });
    107         mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input);
    108         mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
    109 
    110         // divider (only for the new widget style)
    111         mDivider = (TextView) mDelegator.findViewById(R.id.divider);
    112         if (mDivider != null) {
    113             setDividerText();
    114         }
    115 
    116         // minute
    117         mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute);
    118         mMinuteSpinner.setMinValue(0);
    119         mMinuteSpinner.setMaxValue(59);
    120         mMinuteSpinner.setOnLongPressUpdateInterval(100);
    121         mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
    122         mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
    123             public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
    124                 updateInputState();
    125                 int minValue = mMinuteSpinner.getMinValue();
    126                 int maxValue = mMinuteSpinner.getMaxValue();
    127                 if (oldVal == maxValue && newVal == minValue) {
    128                     int newHour = mHourSpinner.getValue() + 1;
    129                     if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) {
    130                         mIsAm = !mIsAm;
    131                         updateAmPmControl();
    132                     }
    133                     mHourSpinner.setValue(newHour);
    134                 } else if (oldVal == minValue && newVal == maxValue) {
    135                     int newHour = mHourSpinner.getValue() - 1;
    136                     if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) {
    137                         mIsAm = !mIsAm;
    138                         updateAmPmControl();
    139                     }
    140                     mHourSpinner.setValue(newHour);
    141                 }
    142                 onTimeChanged();
    143             }
    144         });
    145         mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input);
    146         mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
    147 
    148         // Get the localized am/pm strings and use them in the spinner.
    149         mAmPmStrings = getAmPmStrings(context);
    150 
    151         // am/pm
    152         final View amPmView = mDelegator.findViewById(R.id.amPm);
    153         if (amPmView instanceof Button) {
    154             mAmPmSpinner = null;
    155             mAmPmSpinnerInput = null;
    156             mAmPmButton = (Button) amPmView;
    157             mAmPmButton.setOnClickListener(new View.OnClickListener() {
    158                 public void onClick(View button) {
    159                     button.requestFocus();
    160                     mIsAm = !mIsAm;
    161                     updateAmPmControl();
    162                     onTimeChanged();
    163                 }
    164             });
    165         } else {
    166             mAmPmButton = null;
    167             mAmPmSpinner = (NumberPicker) amPmView;
    168             mAmPmSpinner.setMinValue(0);
    169             mAmPmSpinner.setMaxValue(1);
    170             mAmPmSpinner.setDisplayedValues(mAmPmStrings);
    171             mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
    172                 public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
    173                     updateInputState();
    174                     picker.requestFocus();
    175                     mIsAm = !mIsAm;
    176                     updateAmPmControl();
    177                     onTimeChanged();
    178                 }
    179             });
    180             mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input);
    181             mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
    182         }
    183 
    184         if (isAmPmAtStart()) {
    185             // Move the am/pm view to the beginning
    186             ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout);
    187             amPmParent.removeView(amPmView);
    188             amPmParent.addView(amPmView, 0);
    189             // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme
    190             // for example and not for Holo Theme)
    191             ViewGroup.MarginLayoutParams lp =
    192                     (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams();
    193             final int startMargin = lp.getMarginStart();
    194             final int endMargin = lp.getMarginEnd();
    195             if (startMargin != endMargin) {
    196                 lp.setMarginStart(endMargin);
    197                 lp.setMarginEnd(startMargin);
    198             }
    199         }
    200 
    201         getHourFormatData();
    202 
    203         // update controls to initial state
    204         updateHourControl();
    205         updateMinuteControl();
    206         updateAmPmControl();
    207 
    208         // set to current time
    209         setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
    210         setCurrentMinute(mTempCalendar.get(Calendar.MINUTE));
    211 
    212         if (!isEnabled()) {
    213             setEnabled(false);
    214         }
    215 
    216         // set the content descriptions
    217         setContentDescriptions();
    218 
    219         // If not explicitly specified this view is important for accessibility.
    220         if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
    221             mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    222         }
    223     }
    224 
    225     private void getHourFormatData() {
    226         final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
    227                 (mIs24HourView) ? "Hm" : "hm");
    228         final int lengthPattern = bestDateTimePattern.length();
    229         mHourWithTwoDigit = false;
    230         char hourFormat = '\0';
    231         // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
    232         // the hour format that we found.
    233         for (int i = 0; i < lengthPattern; i++) {
    234             final char c = bestDateTimePattern.charAt(i);
    235             if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
    236                 mHourFormat = c;
    237                 if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
    238                     mHourWithTwoDigit = true;
    239                 }
    240                 break;
    241             }
    242         }
    243     }
    244 
    245     private boolean isAmPmAtStart() {
    246         final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
    247                 "hm" /* skeleton */);
    248 
    249         return bestDateTimePattern.startsWith("a");
    250     }
    251 
    252     /**
    253      * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
    254      *
    255      * See http://unicode.org/cldr/trac/browser/trunk/common/main
    256      *
    257      * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
    258      * separator as the character which is just after the hour marker in the returned pattern.
    259      */
    260     private void setDividerText() {
    261         final String skeleton = (mIs24HourView) ? "Hm" : "hm";
    262         final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
    263                 skeleton);
    264         final String separatorText;
    265         int hourIndex = bestDateTimePattern.lastIndexOf('H');
    266         if (hourIndex == -1) {
    267             hourIndex = bestDateTimePattern.lastIndexOf('h');
    268         }
    269         if (hourIndex == -1) {
    270             // Default case
    271             separatorText = ":";
    272         } else {
    273             int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1);
    274             if  (minuteIndex == -1) {
    275                 separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1));
    276             } else {
    277                 separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex);
    278             }
    279         }
    280         mDivider.setText(separatorText);
    281     }
    282 
    283     @Override
    284     public void setCurrentHour(Integer currentHour) {
    285         setCurrentHour(currentHour, true);
    286     }
    287 
    288     private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) {
    289         // why was Integer used in the first place?
    290         if (currentHour == null || currentHour == getCurrentHour()) {
    291             return;
    292         }
    293         if (!is24HourView()) {
    294             // convert [0,23] ordinal to wall clock display
    295             if (currentHour >= HOURS_IN_HALF_DAY) {
    296                 mIsAm = false;
    297                 if (currentHour > HOURS_IN_HALF_DAY) {
    298                     currentHour = currentHour - HOURS_IN_HALF_DAY;
    299                 }
    300             } else {
    301                 mIsAm = true;
    302                 if (currentHour == 0) {
    303                     currentHour = HOURS_IN_HALF_DAY;
    304                 }
    305             }
    306             updateAmPmControl();
    307         }
    308         mHourSpinner.setValue(currentHour);
    309         if (notifyTimeChanged) {
    310             onTimeChanged();
    311         }
    312     }
    313 
    314     @Override
    315     public Integer getCurrentHour() {
    316         int currentHour = mHourSpinner.getValue();
    317         if (is24HourView()) {
    318             return currentHour;
    319         } else if (mIsAm) {
    320             return currentHour % HOURS_IN_HALF_DAY;
    321         } else {
    322             return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
    323         }
    324     }
    325 
    326     @Override
    327     public void setCurrentMinute(Integer currentMinute) {
    328         if (currentMinute == getCurrentMinute()) {
    329             return;
    330         }
    331         mMinuteSpinner.setValue(currentMinute);
    332         onTimeChanged();
    333     }
    334 
    335     @Override
    336     public Integer getCurrentMinute() {
    337         return mMinuteSpinner.getValue();
    338     }
    339 
    340     @Override
    341     public void setIs24HourView(Boolean is24HourView) {
    342         if (mIs24HourView == is24HourView) {
    343             return;
    344         }
    345         // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
    346         int currentHour = getCurrentHour();
    347         // Order is important here.
    348         mIs24HourView = is24HourView;
    349         getHourFormatData();
    350         updateHourControl();
    351         // set value after spinner range is updated
    352         setCurrentHour(currentHour, false);
    353         updateMinuteControl();
    354         updateAmPmControl();
    355     }
    356 
    357     @Override
    358     public boolean is24HourView() {
    359         return mIs24HourView;
    360     }
    361 
    362     @Override
    363     public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) {
    364         mOnTimeChangedListener = onTimeChangedListener;
    365     }
    366 
    367     @Override
    368     public void setEnabled(boolean enabled) {
    369         mMinuteSpinner.setEnabled(enabled);
    370         if (mDivider != null) {
    371             mDivider.setEnabled(enabled);
    372         }
    373         mHourSpinner.setEnabled(enabled);
    374         if (mAmPmSpinner != null) {
    375             mAmPmSpinner.setEnabled(enabled);
    376         } else {
    377             mAmPmButton.setEnabled(enabled);
    378         }
    379         mIsEnabled = enabled;
    380     }
    381 
    382     @Override
    383     public boolean isEnabled() {
    384         return mIsEnabled;
    385     }
    386 
    387     @Override
    388     public int getBaseline() {
    389         return mHourSpinner.getBaseline();
    390     }
    391 
    392     @Override
    393     public void onConfigurationChanged(Configuration newConfig) {
    394         setCurrentLocale(newConfig.locale);
    395     }
    396 
    397     @Override
    398     public Parcelable onSaveInstanceState(Parcelable superState) {
    399         return new SavedState(superState, getCurrentHour(), getCurrentMinute());
    400     }
    401 
    402     @Override
    403     public void onRestoreInstanceState(Parcelable state) {
    404         SavedState ss = (SavedState) state;
    405         setCurrentHour(ss.getHour());
    406         setCurrentMinute(ss.getMinute());
    407     }
    408 
    409     @Override
    410     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    411         onPopulateAccessibilityEvent(event);
    412         return true;
    413     }
    414 
    415     @Override
    416     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    417         int flags = DateUtils.FORMAT_SHOW_TIME;
    418         if (mIs24HourView) {
    419             flags |= DateUtils.FORMAT_24HOUR;
    420         } else {
    421             flags |= DateUtils.FORMAT_12HOUR;
    422         }
    423         mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour());
    424         mTempCalendar.set(Calendar.MINUTE, getCurrentMinute());
    425         String selectedDateUtterance = DateUtils.formatDateTime(mContext,
    426                 mTempCalendar.getTimeInMillis(), flags);
    427         event.getText().add(selectedDateUtterance);
    428     }
    429 
    430     @Override
    431     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    432         event.setClassName(TimePicker.class.getName());
    433     }
    434 
    435     @Override
    436     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    437         info.setClassName(TimePicker.class.getName());
    438     }
    439 
    440     private void updateInputState() {
    441         // Make sure that if the user changes the value and the IME is active
    442         // for one of the inputs if this widget, the IME is closed. If the user
    443         // changed the value via the IME and there is a next input the IME will
    444         // be shown, otherwise the user chose another means of changing the
    445         // value and having the IME up makes no sense.
    446         InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
    447         if (inputMethodManager != null) {
    448             if (inputMethodManager.isActive(mHourSpinnerInput)) {
    449                 mHourSpinnerInput.clearFocus();
    450                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
    451             } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
    452                 mMinuteSpinnerInput.clearFocus();
    453                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
    454             } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
    455                 mAmPmSpinnerInput.clearFocus();
    456                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
    457             }
    458         }
    459     }
    460 
    461     private void updateAmPmControl() {
    462         if (is24HourView()) {
    463             if (mAmPmSpinner != null) {
    464                 mAmPmSpinner.setVisibility(View.GONE);
    465             } else {
    466                 mAmPmButton.setVisibility(View.GONE);
    467             }
    468         } else {
    469             int index = mIsAm ? Calendar.AM : Calendar.PM;
    470             if (mAmPmSpinner != null) {
    471                 mAmPmSpinner.setValue(index);
    472                 mAmPmSpinner.setVisibility(View.VISIBLE);
    473             } else {
    474                 mAmPmButton.setText(mAmPmStrings[index]);
    475                 mAmPmButton.setVisibility(View.VISIBLE);
    476             }
    477         }
    478         mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
    479     }
    480 
    481     /**
    482      * Sets the current locale.
    483      *
    484      * @param locale The current locale.
    485      */
    486     @Override
    487     public void setCurrentLocale(Locale locale) {
    488         super.setCurrentLocale(locale);
    489         mTempCalendar = Calendar.getInstance(locale);
    490     }
    491 
    492     private void onTimeChanged() {
    493         mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
    494         if (mOnTimeChangedListener != null) {
    495             mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(),
    496                     getCurrentMinute());
    497         }
    498     }
    499 
    500     private void updateHourControl() {
    501         if (is24HourView()) {
    502             // 'k' means 1-24 hour
    503             if (mHourFormat == 'k') {
    504                 mHourSpinner.setMinValue(1);
    505                 mHourSpinner.setMaxValue(24);
    506             } else {
    507                 mHourSpinner.setMinValue(0);
    508                 mHourSpinner.setMaxValue(23);
    509             }
    510         } else {
    511             // 'K' means 0-11 hour
    512             if (mHourFormat == 'K') {
    513                 mHourSpinner.setMinValue(0);
    514                 mHourSpinner.setMaxValue(11);
    515             } else {
    516                 mHourSpinner.setMinValue(1);
    517                 mHourSpinner.setMaxValue(12);
    518             }
    519         }
    520         mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null);
    521     }
    522 
    523     private void updateMinuteControl() {
    524         if (is24HourView()) {
    525             mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
    526         } else {
    527             mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
    528         }
    529     }
    530 
    531     private void setContentDescriptions() {
    532         // Minute
    533         trySetContentDescription(mMinuteSpinner, R.id.increment,
    534                 R.string.time_picker_increment_minute_button);
    535         trySetContentDescription(mMinuteSpinner, R.id.decrement,
    536                 R.string.time_picker_decrement_minute_button);
    537         // Hour
    538         trySetContentDescription(mHourSpinner, R.id.increment,
    539                 R.string.time_picker_increment_hour_button);
    540         trySetContentDescription(mHourSpinner, R.id.decrement,
    541                 R.string.time_picker_decrement_hour_button);
    542         // AM/PM
    543         if (mAmPmSpinner != null) {
    544             trySetContentDescription(mAmPmSpinner, R.id.increment,
    545                     R.string.time_picker_increment_set_pm_button);
    546             trySetContentDescription(mAmPmSpinner, R.id.decrement,
    547                     R.string.time_picker_decrement_set_am_button);
    548         }
    549     }
    550 
    551     private void trySetContentDescription(View root, int viewId, int contDescResId) {
    552         View target = root.findViewById(viewId);
    553         if (target != null) {
    554             target.setContentDescription(mContext.getString(contDescResId));
    555         }
    556     }
    557 
    558     /**
    559      * Used to save / restore state of time picker
    560      */
    561     private static class SavedState extends View.BaseSavedState {
    562         private final int mHour;
    563         private final int mMinute;
    564 
    565         private SavedState(Parcelable superState, int hour, int minute) {
    566             super(superState);
    567             mHour = hour;
    568             mMinute = minute;
    569         }
    570 
    571         private SavedState(Parcel in) {
    572             super(in);
    573             mHour = in.readInt();
    574             mMinute = in.readInt();
    575         }
    576 
    577         public int getHour() {
    578             return mHour;
    579         }
    580 
    581         public int getMinute() {
    582             return mMinute;
    583         }
    584 
    585         @Override
    586         public void writeToParcel(Parcel dest, int flags) {
    587             super.writeToParcel(dest, flags);
    588             dest.writeInt(mHour);
    589             dest.writeInt(mMinute);
    590         }
    591 
    592         @SuppressWarnings({"unused", "hiding"})
    593         public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
    594             public SavedState createFromParcel(Parcel in) {
    595                 return new SavedState(in);
    596             }
    597 
    598             public SavedState[] newArray(int size) {
    599                 return new SavedState[size];
    600             }
    601         };
    602     }
    603 
    604     public static String[] getAmPmStrings(Context context) {
    605         String[] result = new String[2];
    606         LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
    607         result[0] = d.amPm[0].length() > 2 ? d.narrowAm : d.amPm[0];
    608         result[1] = d.amPm[1].length() > 2 ? d.narrowPm : d.amPm[1];
    609         return result;
    610     }
    611 }
    612