Skip to content

Commit

Permalink
feat: add Ti.Local.parseDecimal() method (#11683)
Browse files Browse the repository at this point in the history
- Parses a localized numeric string to a number.

Fixes TIMOB-27874
  • Loading branch information
jquick-axway committed Jul 13, 2020
1 parent 43a20a3 commit 6253813
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package ti.modules.titanium.locale;

import java.util.Locale;
import java.text.NumberFormat;

import org.appcelerator.kroll.KrollModule;
import org.appcelerator.kroll.annotations.Kroll;
Expand Down Expand Up @@ -82,6 +83,39 @@ public String formatTelephoneNumber(String telephoneNumber)
return PhoneNumberUtils.formatNumber(telephoneNumber);
}

@Kroll.method
public double parseDecimal(String text, @Kroll.argument(optional = true) String localeString)
{
double result = Double.NaN;
try {
// Create a number format parser using given locale if provided or current locale.
Locale locale = TiPlatformHelper.getInstance().getLocale(localeString);
NumberFormat numberFormat;
if (locale != null) {
numberFormat = NumberFormat.getInstance(locale);
} else {
numberFormat = NumberFormat.getInstance();
}

// Enable thousands separator parsing support. (ex: "1,234,567")
numberFormat.setGroupingUsed(true);

// Remove leading spaces and plus sign. Number format will fail to parse if there.
text = text.trim();
if ((text != null) && text.startsWith("+")) {
text = text.substring(1);
}

// Attempt to parse a decimal value from given string.
Number number = numberFormat.parse(text);
if (number != null) {
result = number.doubleValue();
}
} catch (Exception ex) {
}
return result;
}

@Kroll.method
@Kroll.setProperty
public void setLanguage(String language)
Expand Down
3 changes: 3 additions & 0 deletions apidoc/Global/String/String.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ methods:
summary: |
Formats a number into the decimal format, including decimal symbol, of the locale
configured for the system.
description: |
The localized string created by this method can be parsed be parsed back to a number
via the <Titanium.Locale.parseDecimal> method.
returns:
type: String
summary: |
Expand Down
31 changes: 31 additions & 0 deletions apidoc/Titanium/Locale/Locale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,37 @@ methods:
Ti.API.info(String.format(L('phrase'), L('greeting', i18nMissingMsg), L('signoff', i18nMissingMsg)));
```
- name: parseDecimal
summary: Parses a number from the given string using the current or given locale.
platforms: [android, iphone, ipad]
returns:
type: Number
summary: Returns the parsed number. Returns `NaN` (Not-a-Number) if given string is invalid.
parameters:
- name: text
summary: The string to be parsed.
type: String

- name: locale
summary: |
Locale, as a combination of ISO 2-letter language and country codes. For example,
`en-US` or `en-GB`. See the
[ISO 639-1](http://en.wikipedia.org/wiki/ISO_639-1) and
[ISO 3166-1 alpha-2](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements)
sections of wikipedia for reference.
type: String
optional: true
examples:
- title: Parsing Localized Numeric Strings
example: |
// You can convert to/from a localized numeric string like this.
var numericString = String.formatDecimal(1234.5);
var numericValue = Ti.Locale.parseDecimal(numericString);
// The numbers returned will be the same for the 2 different locales.
var number1 = Ti.Locale.parseDecimal('1,234,567.8', 'en-US');
var number2 = Ti.Locale.parseDecimal('1.234.567,8', 'de-DE');
events:
- name: change
summary: Fired when the device locale changes.
Expand Down
4 changes: 4 additions & 0 deletions iphone/Classes/LocaleModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ JSExportAs(getString,
-(NSString *)getString
: (NSString *)key withHint
: (id)hint);
JSExportAs(parseDecimal,
-(NSNumber *)parseDecimal
: (NSString *)text withLocaleId
: (id)localeId);
- (void)setLanguage:(NSString *)language;

@end
Expand Down
29 changes: 29 additions & 0 deletions iphone/Classes/LocaleModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ - (NSString *)getLocaleCurrencySymbol:(NSString *)locale
return [[[[NSLocale alloc] initWithLocaleIdentifier:locale] autorelease] objectForKey:NSLocaleCurrencySymbol];
}

- (NSNumber *)parseDecimal:(NSString *)text withLocaleId:(id)localeId
{
// Fetch optional locale string argument. (ex: "en-US")
NSString *localeStringId = nil;
if ((localeId != nil) && [localeId isKindOfClass:[NSString class]]) {
localeStringId = (NSString *)localeId;
}

// Acquire requested locale if provided or current locale.
NSLocale *locale = nil;
if (localeStringId != nil) {
locale = [[[NSLocale alloc] initWithLocaleIdentifier:localeStringId] autorelease];
}
if (locale == nil) {
locale = [NSLocale currentLocale];
}

// Remove localized thousands separators from string if they exist. NSScanner fails to parse them.
NSString *thousandsSeparator = [locale objectForKey:NSLocaleGroupingSeparator];
text = [text stringByReplacingOccurrencesOfString:thousandsSeparator withString:@""];

// Attempt to parse a number from given text. Return not-a-number if failed.
NSScanner *scanner = [NSScanner localizedScannerWithString:text];
[scanner setLocale:locale];
double value = NAN;
[scanner scanDouble:&value];
return [NSNumber numberWithDouble:value];
}

- (void)setLanguage:(NSString *)locale
{
[TiLocale setLocale:locale];
Expand Down
118 changes: 118 additions & 0 deletions tests/Resources/ti.locale.addontest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* Appcelerator Titanium Mobile
* Copyright (c) 2015-Present by Axway, Inc. All Rights Reserved.
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
/* eslint-env mocha */
/* eslint no-unused-expressions: "off" */
'use strict';
var should = require('./utilities/assertions');

describe('Titanium.Locale', function () {
describe('#parseDecimal()', () => {
it('compared with String.formatDecimal()', () => {
should(Ti.Locale.parseDecimal).be.a.Function();

const numericValue = 1234567.8;

let numericString = String.formatDecimal(numericValue);
let parsedValue = Ti.Locale.parseDecimal(numericString);
should(parsedValue).be.a.Number();
should(Math.abs(parsedValue - numericValue)).be.lessThan(Number.EPSILON);

numericString = String.formatDecimal(numericValue, 'de-DE');
parsedValue = Ti.Locale.parseDecimal(numericString, 'de-DE');
should(Math.abs(parsedValue - numericValue)).be.lessThan(Number.EPSILON);

numericString = String.formatDecimal(numericValue, 'fr-FR');
parsedValue = Ti.Locale.parseDecimal(numericString, 'fr-FR');
should(Math.abs(parsedValue - numericValue)).be.lessThan(Number.EPSILON);

numericString = String.formatDecimal(numericValue, 'ar-EG');
parsedValue = Ti.Locale.parseDecimal(numericString, 'ar-EG');
should(Math.abs(parsedValue - numericValue)).be.lessThan(Number.EPSILON);
});

it('localized values', () => {
let result = Ti.Locale.parseDecimal('1,234,567.8', 'en-US');
should(Math.abs(result - 1234567.8)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('1.234.567,8', 'de-DE');
should(Math.abs(result - 1234567.8)).be.lessThan(Number.EPSILON);

// France uses non-breaking unicode spaces for thousands separator.
result = Ti.Locale.parseDecimal('1\u00A0234\u00A0567,8', 'fr-FR');
should(Math.abs(result - 1234567.8)).be.lessThan(Number.EPSILON);
});

it('various values', () => {
let result = Ti.Locale.parseDecimal('0', 'en-US');
should(Math.abs(result)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('0.', 'en-US');
should(Math.abs(result)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('.0', 'en-US');
should(Math.abs(result)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('00.00123', 'en-US');
should(Math.abs(result - 0.00123)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('+0', 'en-US');
should(Math.abs(result)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('-0', 'en-US');
should(Math.abs(result)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('+1234.5', 'en-US');
should(Math.abs(result - 1234.5)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('-1234.5', 'en-US');
should(Math.abs(result + 1234.5)).be.lessThan(Number.EPSILON);
});

it('scientific values', () => {
let result = Ti.Locale.parseDecimal('1.2E+3', 'en-US');
should(Math.abs(result - 1200.0)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('-1.2E+3', 'en-US');
should(Math.abs(result + 1200.0)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('1.2E-3', 'en-US');
should(Math.abs(result - 0.0012)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal('-1.2E-3', 'en-US');
should(Math.abs(result + 0.0012)).be.lessThan(Number.EPSILON);
});

it('padded with spaces', () => {
let result = Ti.Locale.parseDecimal(' 123 ', 'en-US');
should(Math.abs(result - 123)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal(' +123 ', 'en-US');
should(Math.abs(result - 123)).be.lessThan(Number.EPSILON);

result = Ti.Locale.parseDecimal(' -123 ', 'en-US');
should(Math.abs(result + 123)).be.lessThan(Number.EPSILON);
});

it('NaN', () => {
let result = Ti.Locale.parseDecimal('ThisShouldFail');
should(result).be.a.Number();
should(result).be.eql(Number.NaN);

result = Ti.Locale.parseDecimal('');
should(result).be.eql(Number.NaN);

result = Ti.Locale.parseDecimal(' ');
should(result).be.eql(Number.NaN);

result = Ti.Locale.parseDecimal(null);
should(result).be.eql(Number.NaN);

result = Ti.Locale.parseDecimal(undefined);
should(result).be.eql(Number.NaN);
});
});
});

0 comments on commit 6253813

Please sign in to comment.