|
10 | 10 | * governing permissions and limitations under the License.
|
11 | 11 | */
|
12 | 12 |
|
| 13 | +import fc from 'fast-check'; |
| 14 | +import messages from '../../../@react-aria/numberfield/intl/*'; |
13 | 15 | import {NumberParser} from '../src/NumberParser';
|
14 | 16 |
|
| 17 | +// for some reason hu-HU isn't supported in jsdom/node |
| 18 | +let locales = Object.keys(messages).map(locale => locale.replace('.json', '')).filter(locale => locale !== 'hu-HU'); |
| 19 | + |
15 | 20 | describe('NumberParser', function () {
|
16 | 21 | describe('parse', function () {
|
17 | 22 | it('should support basic numbers', function () {
|
@@ -155,10 +160,117 @@ describe('NumberParser', function () {
|
155 | 160 | });
|
156 | 161 |
|
157 | 162 | it('should parse a percent with decimals', function () {
|
158 |
| - expect(new NumberParser('en-US', {style: 'percent'}).parse('10.5%')).toBe(0.1); |
| 163 | + expect(new NumberParser('en-US', {style: 'percent'}).parse('10.5%')).toBe(0.11); |
159 | 164 | expect(new NumberParser('en-US', {style: 'percent', minimumFractionDigits: 2}).parse('10.5%')).toBe(0.105);
|
160 | 165 | });
|
161 | 166 | });
|
| 167 | + |
| 168 | + describe('round trips', function () { |
| 169 | + // Locales have to include: 'de-DE', 'ar-EG', 'fr-FR' and possibly others |
| 170 | + // But for the moment they are not properly supported |
| 171 | + const localesArb = fc.constantFrom(...locales); |
| 172 | + const styleOptsArb = fc.oneof( |
| 173 | + {withCrossShrink: true}, |
| 174 | + fc.record({style: fc.constant('decimal')}), |
| 175 | + // 'percent' should be part of the possible options, but for the moment it fails for some tests |
| 176 | + fc.record({style: fc.constant('percent')}), |
| 177 | + fc.record( |
| 178 | + {style: fc.constant('currency'), currency: fc.constantFrom('USD', 'EUR', 'CNY', 'JPY'), currencyDisplay: fc.constantFrom('symbol', 'code', 'name')}, |
| 179 | + {requiredKeys: ['style', 'currency']} |
| 180 | + ), |
| 181 | + fc.record( |
| 182 | + {style: fc.constant('unit'), unit: fc.constantFrom('inch', 'liter', 'kilometer-per-hour')}, |
| 183 | + {requiredKeys: ['style', 'unit']} |
| 184 | + ) |
| 185 | + ); |
| 186 | + const genericOptsArb = fc.record({ |
| 187 | + localeMatcher: fc.constantFrom('best fit', 'lookup'), |
| 188 | + unitDisplay: fc.constantFrom('narrow', 'short', 'long'), |
| 189 | + useGrouping: fc.boolean(), |
| 190 | + minimumIntegerDigits: fc.integer({min: 1, max: 21}), |
| 191 | + minimumFractionDigits: fc.integer({min: 0, max: 20}), |
| 192 | + maximumFractionDigits: fc.integer({min: 0, max: 20}), |
| 193 | + minimumSignificantDigits: fc.integer({min: 1, max: 21}), |
| 194 | + maximumSignificantDigits: fc.integer({min: 1, max: 21}) |
| 195 | + }, {requiredKeys: []}); |
| 196 | + |
| 197 | + // We restricted the set of possible values to avoid unwanted overflows to infinity and underflows to zero |
| 198 | + // and stay in the domain of legit values. |
| 199 | + const DOUBLE_MIN = Number.EPSILON; |
| 200 | + const valueArb = fc.tuple( |
| 201 | + fc.constantFrom(1, -1), |
| 202 | + fc.double({next: true, noNaN: true, min: DOUBLE_MIN, max: 1 / DOUBLE_MIN}) |
| 203 | + ).map(([sign, value]) => sign * value); |
| 204 | + |
| 205 | + const inputsArb = fc.tuple(valueArb, localesArb, styleOptsArb, genericOptsArb) |
| 206 | + .map(([d, locale, styleOpts, genericOpts]) => ({d, opts: {...styleOpts, ...genericOpts}, locale})) |
| 207 | + .filter(({opts}) => opts.minimumFractionDigits === undefined || opts.maximumFractionDigits === undefined || opts.minimumFractionDigits <= opts.maximumFractionDigits) |
| 208 | + .filter(({opts}) => opts.minimumSignificantDigits === undefined || opts.maximumSignificantDigits === undefined || opts.minimumSignificantDigits <= opts.maximumSignificantDigits) |
| 209 | + .map(({d, opts, locale}) => { |
| 210 | + if (opts.style === 'percent') { |
| 211 | + opts.minimumFractionDigits = opts.minimumFractionDigits > 18 ? 18 : opts.minimumFractionDigits; |
| 212 | + opts.maximumFractionDigits = opts.maximumFractionDigits > 18 ? 18 : opts.maximumFractionDigits; |
| 213 | + } |
| 214 | + return {d, opts, locale}; |
| 215 | + }) |
| 216 | + .map(({d, opts, locale}) => { |
| 217 | + let adjustedNumberForFractions = d; |
| 218 | + if (Math.abs(d) < 1 && opts.minimumFractionDigits && opts.minimumFractionDigits > 1) { |
| 219 | + adjustedNumberForFractions = d * (10 ** (opts.minimumFractionDigits || 2)); |
| 220 | + } else if (Math.abs(d) > 1 && opts.minimumFractionDigits && opts.minimumFractionDigits > 1) { |
| 221 | + adjustedNumberForFractions = d / (10 ** (opts.minimumFractionDigits || 2)); |
| 222 | + } |
| 223 | + return {adjustedNumberForFractions, opts, locale}; |
| 224 | + }); |
| 225 | + |
| 226 | + // skipping until we can reliably run it, until then, it's good to run manually |
| 227 | + // track counter examples below |
| 228 | + it.skip('should fully reverse NumberFormat', function () { |
| 229 | + fc.assert( |
| 230 | + fc.property( |
| 231 | + inputsArb, |
| 232 | + function ({adjustedNumberForFractions, locale, opts}) { |
| 233 | + const formatter = new Intl.NumberFormat(locale, opts); |
| 234 | + const parser = new NumberParser(locale, opts); |
| 235 | + |
| 236 | + const formattedOnce = formatter.format(adjustedNumberForFractions); |
| 237 | + expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); |
| 238 | + } |
| 239 | + ) |
| 240 | + ); |
| 241 | + }); |
| 242 | + }); |
| 243 | + describe('counter examples', () => { |
| 244 | + it('can still get all plural literals with minimum significant digits', () => { |
| 245 | + let locale = 'pl-PL'; |
| 246 | + let options = { |
| 247 | + style: 'unit', |
| 248 | + unit: 'inch', |
| 249 | + minimumSignificantDigits: 4, |
| 250 | + maximumSignificantDigits: 6 |
| 251 | + }; |
| 252 | + const formatter = new Intl.NumberFormat(locale, options); |
| 253 | + const parser = new NumberParser(locale, options); |
| 254 | + |
| 255 | + const formattedOnce = formatter.format(60048.95); |
| 256 | + expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); |
| 257 | + }); |
| 258 | + // See Bug https://github.com/nodejs/node/issues/49919 |
| 259 | + it.skip('formatted units keep their number', () => { |
| 260 | + let locale = 'da-DK'; |
| 261 | + let options = { |
| 262 | + style: 'unit', |
| 263 | + unit: 'kilometer-per-hour', |
| 264 | + unitDisplay: 'long', |
| 265 | + minimumSignificantDigits: 1 |
| 266 | + }; |
| 267 | + const formatter = new Intl.NumberFormat(locale, options); |
| 268 | + const parser = new NumberParser(locale, options); |
| 269 | + |
| 270 | + const formattedOnce = formatter.format(1); |
| 271 | + expect(formatter.format(parser.parse(formattedOnce))).toBe(formattedOnce); |
| 272 | + }); |
| 273 | + }); |
162 | 274 | });
|
163 | 275 |
|
164 | 276 | describe('isValidPartialNumber', function () {
|
|
0 commit comments