Skip to content

Commit

Permalink
feat(android): add missing options to Intl.DateTimeFormat.resolvedOpt…
Browse files Browse the repository at this point in the history
…ions() (#12377)

Fixes TIMOB-28251
  • Loading branch information
build committed Jan 8, 2021
1 parent 0b83420 commit 6aa7c83
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
package ti.modules.titanium.locale;

import android.os.Build;
import java.text.AttributedCharacterIterator;
import java.text.DateFormat;
import java.text.FieldPosition;
Expand Down Expand Up @@ -42,8 +43,23 @@ public class DateTimeFormatProxy extends KrollProxy
private static final String NUMERIC_STRING_ID = "numeric";
private static final String TWO_DIGIT_STRING_ID = "2-digit";

private static final String WEEKDAY_STRING_ID = "weekday";
private static final String ERA_STRING_ID = "era";
private static final String YEAR_STRING_ID = "year";
private static final String MONTH_STRING_ID = "month";
private static final String DAY_STRING_ID = "day";
private static final String HOUR12_STRING_ID = "hour12";
private static final String HOUR_CYCLE_STRING_ID = "hourCycle";
private static final String HOUR_STRING_ID = "hour";
private static final String MINUTE_STRING_ID = "minute";
private static final String SECOND_STRING_ID = "second";
private static final String FRACTIONAL_SECOND_DIGITS_STRING_ID = "fractionalSecondDigits";
private static final String DAY_PERIOD_STRING_ID = "dayPeriod";
private static final String TIME_ZONE_NAME_STRING_ID = "timeZoneName";

private DateFormat dateFormat = DateFormat.getInstance();
private KrollDict resolvedOptions = new KrollDict();
private boolean hasUpdatedResolvedOptions;

@Override
public void handleCreationDict(KrollDict properties)
Expand Down Expand Up @@ -71,30 +87,30 @@ public void handleCreationDict(KrollDict properties)
}

// Fetch main date/time component properties.
String weekdayFormatId = TiConvert.toString(options.get("weekday"));
String eraFormatId = TiConvert.toString(options.get("era"));
String yearFormatId = TiConvert.toString(options.get("year"));
String monthFormatId = TiConvert.toString(options.get("month"));
String dayFormatId = TiConvert.toString(options.get("day"));
String hourFormatId = TiConvert.toString(options.get("hour"));
String minuteFormatId = TiConvert.toString(options.get("minute"));
String secondFormatId = TiConvert.toString(options.get("second"));
String dayPeriodFormatId = TiConvert.toString(options.get("dayPeriod"));
String timeZoneFormatId = TiConvert.toString(options.get("timeZoneName"));
String weekdayFormatId = TiConvert.toString(options.get(WEEKDAY_STRING_ID));
String eraFormatId = TiConvert.toString(options.get(ERA_STRING_ID));
String yearFormatId = TiConvert.toString(options.get(YEAR_STRING_ID));
String monthFormatId = TiConvert.toString(options.get(MONTH_STRING_ID));
String dayFormatId = TiConvert.toString(options.get(DAY_STRING_ID));
String hourFormatId = TiConvert.toString(options.get(HOUR_STRING_ID));
String minuteFormatId = TiConvert.toString(options.get(MINUTE_STRING_ID));
String secondFormatId = TiConvert.toString(options.get(SECOND_STRING_ID));
String dayPeriodFormatId = TiConvert.toString(options.get(DAY_PERIOD_STRING_ID));
String timeZoneFormatId = TiConvert.toString(options.get(TIME_ZONE_NAME_STRING_ID));

// Fetch hour handling.
// Note: The "hour12" boolean must overriding the "hourCycle" property.
String hourCycleFormatId = TiConvert.toString(options.get("hourCycle"));
if (options.containsKey("hour12")) {
if (TiConvert.toBoolean(options.get("hour12"), true)) {
String hourCycleFormatId = TiConvert.toString(options.get(HOUR_CYCLE_STRING_ID));
if (options.containsKey(HOUR12_STRING_ID)) {
if (TiConvert.toBoolean(options.get(HOUR12_STRING_ID), true)) {
hourCycleFormatId = "h12";
} else {
hourCycleFormatId = "h23";
}
}

// Fetch number of millisecond digits to display.
int millisecondDigits = TiConvert.toInt(options.get("fractionalSecondDigits"), 0);
int millisecondDigits = TiConvert.toInt(options.get(FRACTIONAL_SECOND_DIGITS_STRING_ID), 0);
if (millisecondDigits < 0) {
millisecondDigits = 0;
} else if (millisecondDigits > 3) {
Expand Down Expand Up @@ -194,11 +210,17 @@ public void handleCreationDict(KrollDict properties)
this.resolvedOptions = new KrollDict();
this.resolvedOptions.putAll(options);
this.resolvedOptions.put(TiC.PROPERTY_LOCALE, locale.toString().replace("_", "-"));
this.resolvedOptions.put("timeZone", this.dateFormat.getTimeZone().getID());
NumberingSystem numberingSystem = NumberingSystem.from(locale);
if (numberingSystem == null) {
numberingSystem = NumberingSystem.LATN;
}
this.resolvedOptions.put(TiC.PROPERTY_NUMBERING_SYSTEM, numberingSystem.toLdmlStringId());
String calendarStringId = "gregory";
if (Build.VERSION.SDK_INT >= 26) {
calendarStringId = this.dateFormat.getCalendar().getCalendarType();
}
this.resolvedOptions.put("calendar", calendarStringId);
}

@Kroll.method
Expand Down Expand Up @@ -250,31 +272,31 @@ public KrollDict[] formatToParts(Date value)
// Get the JavaScript "Intl.DateTimeFormat" part type equivalent.
Format.Field formatField = fieldPosition.getFieldAttribute();
if (formatField == DateFormat.Field.AM_PM) {
typeName = "dayPeriod";
typeName = DAY_PERIOD_STRING_ID;
} else if (formatField == DateFormat.Field.DAY_OF_MONTH) {
typeName = "day";
typeName = DAY_STRING_ID;
} else if ((formatField == DateFormat.Field.DAY_OF_WEEK)
|| (formatField == DateFormat.Field.DAY_OF_WEEK_IN_MONTH)) {
typeName = "weekday";
typeName = WEEKDAY_STRING_ID;
} else if (formatField == DateFormat.Field.ERA) {
typeName = "era";
typeName = ERA_STRING_ID;
} else if ((formatField == DateFormat.Field.HOUR0)
|| (formatField == DateFormat.Field.HOUR1)
|| (formatField == DateFormat.Field.HOUR_OF_DAY0)
|| (formatField == DateFormat.Field.HOUR_OF_DAY1)) {
typeName = "hour";
typeName = HOUR_STRING_ID;
} else if (formatField == DateFormat.Field.MILLISECOND) {
typeName = "fractionalSecond";
} else if (formatField == DateFormat.Field.MINUTE) {
typeName = "minute";
typeName = MINUTE_STRING_ID;
} else if (formatField == DateFormat.Field.MONTH) {
typeName = "month";
typeName = MONTH_STRING_ID;
} else if (formatField == DateFormat.Field.SECOND) {
typeName = "second";
typeName = SECOND_STRING_ID;
} else if (formatField == DateFormat.Field.TIME_ZONE) {
typeName = "timeZoneName";
typeName = TIME_ZONE_NAME_STRING_ID;
} else if (formatField == DateFormat.Field.YEAR) {
typeName = "year";
typeName = YEAR_STRING_ID;
} else {
typeName = "literal";
}
Expand Down Expand Up @@ -308,9 +330,156 @@ public KrollDict[] formatToParts(Date value)
@Kroll.method
public KrollDict resolvedOptions()
{
// Parse for the rest of the options from the date/time pattern, if not done already.
if (!this.hasUpdatedResolvedOptions) {
if (this.dateFormat instanceof SimpleDateFormat) {
updateResolvedOptionsWithPattern(((SimpleDateFormat) this.dateFormat).toPattern());
}
this.hasUpdatedResolvedOptions = true;
}

// Return a dictionary describing how date/time is formatted.
return this.resolvedOptions;
}

private void updateResolvedOptionsWithPattern(String pattern)
{
// Validate.
if ((this.resolvedOptions == null) || (pattern == null)) {
return;
}

// First, remove all date/time options before parsing for them down below.
// We do this in case these options are not found in the pattern.
this.resolvedOptions.remove(ERA_STRING_ID);
this.resolvedOptions.remove(YEAR_STRING_ID);
this.resolvedOptions.remove(MONTH_STRING_ID);
this.resolvedOptions.remove(DAY_STRING_ID);
this.resolvedOptions.remove(WEEKDAY_STRING_ID);
this.resolvedOptions.remove(DAY_PERIOD_STRING_ID);
this.resolvedOptions.remove(HOUR12_STRING_ID);
this.resolvedOptions.remove(HOUR_CYCLE_STRING_ID);
this.resolvedOptions.remove(HOUR_STRING_ID);
this.resolvedOptions.remove(MINUTE_STRING_ID);
this.resolvedOptions.remove(SECOND_STRING_ID);
this.resolvedOptions.remove(FRACTIONAL_SECOND_DIGITS_STRING_ID);
this.resolvedOptions.remove(TIME_ZONE_NAME_STRING_ID);

// Parse pattern and update resolved options for each date/time component found.
int charCount = 0;
char lastChar = '\0';
final int PATTERN_LENGTH = pattern.length();
for (int index = 0; index <= PATTERN_LENGTH; index++) {
// Fetch next character in pattern.
char nextChar = (index < PATTERN_LENGTH) ? pattern.charAt(index) : '\0';

// Skip to next character until duplicate character sequence has ended.
if (charCount <= 0) {
lastChar = nextChar;
charCount = 1;
continue;
} else if (nextChar == lastChar) {
charCount++;
continue;
}

// The duplicate character sequence has ended.
// Check if it's a known pattern and update resolved options dictionary.
switch (lastChar) {
case 'G':
if (charCount >= 3) {
this.resolvedOptions.put(ERA_STRING_ID, LONG_STRING_ID);
} else if (charCount == 2) {
this.resolvedOptions.put(ERA_STRING_ID, SHORT_STRING_ID);
} else {
this.resolvedOptions.put(ERA_STRING_ID, NARROW_STRING_ID);
}
break;
case 'y':
case 'Y':
this.resolvedOptions.put(YEAR_STRING_ID, charCount == 2 ? TWO_DIGIT_STRING_ID : NUMERIC_STRING_ID);
break;
case 'M':
case 'L':
if (charCount >= 5) {
this.resolvedOptions.put(MONTH_STRING_ID, NARROW_STRING_ID);
} else if (charCount == 4) {
this.resolvedOptions.put(MONTH_STRING_ID, LONG_STRING_ID);
} else if (charCount == 3) {
this.resolvedOptions.put(MONTH_STRING_ID, SHORT_STRING_ID);
} else if (charCount == 2) {
this.resolvedOptions.put(MONTH_STRING_ID, TWO_DIGIT_STRING_ID);
} else {
this.resolvedOptions.put(MONTH_STRING_ID, NUMERIC_STRING_ID);
}
break;
case 'd':
this.resolvedOptions.put(DAY_STRING_ID, charCount == 2 ? TWO_DIGIT_STRING_ID : NUMERIC_STRING_ID);
break;
case 'E':
case 'c':
if (charCount >= 5) {
this.resolvedOptions.put(WEEKDAY_STRING_ID, NARROW_STRING_ID);
} else if (charCount == 4) {
this.resolvedOptions.put(WEEKDAY_STRING_ID, LONG_STRING_ID);
} else {
this.resolvedOptions.put(WEEKDAY_STRING_ID, SHORT_STRING_ID);
}
break;
case 'a':
if (charCount >= 3) {
this.resolvedOptions.put(DAY_PERIOD_STRING_ID, LONG_STRING_ID);
} else if (charCount == 2) {
this.resolvedOptions.put(DAY_PERIOD_STRING_ID, SHORT_STRING_ID);
} else {
this.resolvedOptions.put(DAY_PERIOD_STRING_ID, NARROW_STRING_ID);
}
break;
case 'H':
this.resolvedOptions.put(HOUR12_STRING_ID, false);
this.resolvedOptions.put(HOUR_CYCLE_STRING_ID, "h23");
this.resolvedOptions.put(HOUR_STRING_ID, charCount == 2 ? TWO_DIGIT_STRING_ID : NUMERIC_STRING_ID);
break;
case 'h':
this.resolvedOptions.put(HOUR12_STRING_ID, true);
this.resolvedOptions.put(HOUR_CYCLE_STRING_ID, "h12");
this.resolvedOptions.put(HOUR_STRING_ID, charCount == 2 ? TWO_DIGIT_STRING_ID : NUMERIC_STRING_ID);
break;
case 'K':
this.resolvedOptions.put(HOUR12_STRING_ID, true);
this.resolvedOptions.put(HOUR_CYCLE_STRING_ID, "h11");
this.resolvedOptions.put(HOUR_STRING_ID, charCount == 2 ? TWO_DIGIT_STRING_ID : NUMERIC_STRING_ID);
break;
case 'k':
this.resolvedOptions.put(HOUR12_STRING_ID, false);
this.resolvedOptions.put(HOUR_CYCLE_STRING_ID, "h24");
this.resolvedOptions.put(HOUR_STRING_ID, charCount == 2 ? TWO_DIGIT_STRING_ID : NUMERIC_STRING_ID);
break;
case 'm':
this.resolvedOptions.put(
MINUTE_STRING_ID, (charCount == 2) ? TWO_DIGIT_STRING_ID : NUMERIC_STRING_ID);
break;
case 's':
this.resolvedOptions.put(
SECOND_STRING_ID, (charCount == 2) ? TWO_DIGIT_STRING_ID : NUMERIC_STRING_ID);
break;
case 'S':
this.resolvedOptions.put(FRACTIONAL_SECOND_DIGITS_STRING_ID, charCount);
break;
case 'z':
case 'Z':
case 'X':
this.resolvedOptions.put(
TIME_ZONE_NAME_STRING_ID, (charCount > 1) ? LONG_STRING_ID : SHORT_STRING_ID);
break;
}

// Start counting the next character sequence.
charCount = 1;
lastChar = nextChar;
}
}

private int getIntIdForStyleId(String stringId)
{
if (stringId != null) {
Expand All @@ -337,7 +506,7 @@ private String getPatternForWeekdayId(String stringId)
case SHORT_STRING_ID:
return "EEE";
case NARROW_STRING_ID:
return "E";
return "EEEEE";
}
}
return "";
Expand Down Expand Up @@ -380,10 +549,11 @@ private String getPatternForMonthId(String stringId)
case TWO_DIGIT_STRING_ID:
return "MM";
case SHORT_STRING_ID:
case NARROW_STRING_ID:
return "MMM";
case LONG_STRING_ID:
return "MMMM";
case NARROW_STRING_ID:
return "MMMMM";
}
}
return "";
Expand Down
61 changes: 56 additions & 5 deletions tests/Resources/intl.datetimeformat.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,62 @@ describe('Intl.DateTimeFormat', () => {
});
});

it('#resolvedOptions()', () => {
const formatter = new Intl.DateTimeFormat();
should(formatter.resolvedOptions).not.be.undefined();
should(formatter.resolvedOptions).be.a.Function();
should(formatter.resolvedOptions()).be.an.Object();
describe('#resolvedOptions()', () => {
it('validate function', () => {
const formatter = new Intl.DateTimeFormat();
should(formatter.resolvedOptions).not.be.undefined();
should(formatter.resolvedOptions).be.a.Function();
should(formatter.resolvedOptions()).be.an.Object();
});

it('assigned from constructor', () => {
const options = {
year: 'numeric',
month: 'long',
day: '2-digit',
weekday: 'long',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZoneName: 'short'
};
if (OS_ANDROID) {
options.dayPeriod = 'narrow';
options.fractionalSecondDigits = 3;
}
const formatter = new Intl.DateTimeFormat('en-US', options);
const result = formatter.resolvedOptions();
for (const key in options) {
should(result[key]).be.eql(options[key]);
}
});

it('timeZone', () => {
should(Intl.DateTimeFormat().resolvedOptions().timeZone).be.a.String();
let formatter = new Intl.DateTimeFormat('en-US', { timeZone: 'UTC' });
should(formatter.resolvedOptions().timeZone).be.eql('UTC');
formatter = new Intl.DateTimeFormat('en-US', { timeZone: 'Etc/GMT+8' });
should(formatter.resolvedOptions().timeZone).be.eql('Etc/GMT+8');
formatter = new Intl.DateTimeFormat('en-US', { timeZone: 'America/Los_Angeles' });
should(formatter.resolvedOptions().timeZone).be.eql('America/Los_Angeles');
});

it.android('dateStyle', () => {
const formatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'long' });
const result = formatter.resolvedOptions();
should(result.day).be.equalOneOf([ 'numeric', '2-digit' ]);
should(result.month).be.equalOneOf([ 'numeric', '2-digit', 'long', 'short', 'narrow' ]);
should(result.year).be.equalOneOf([ 'numeric', '2-digit' ]);
});

it.android('timeStyle', () => {
const formatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'long' });
const result = formatter.resolvedOptions();
should(result.hour).be.equalOneOf([ 'numeric', '2-digit' ]);
should(result.minute).be.equalOneOf([ 'numeric', '2-digit' ]);
should(result.second).be.equalOneOf([ 'numeric', '2-digit' ]);
});
});

describe('#supportedLocalesOf()', () => {
Expand Down

0 comments on commit 6aa7c83

Please sign in to comment.