Skip to content

Commit

Permalink
add regex fallback to splitAddress1 function
Browse files Browse the repository at this point in the history
  • Loading branch information
gabypancu committed Sep 12, 2024
1 parent f66678e commit 7829478
Show file tree
Hide file tree
Showing 19 changed files with 333 additions and 34 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Security in case of vulnerabilities.

## [Unreleased]
- Nil.
- Add address1_regex to regions [#281](https://github.com/Shopify/worldwide/pull/281)

---

Expand Down
2 changes: 2 additions & 0 deletions db/data/regions/NL.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ format:
show: "{firstName} {lastName}_{company}_{address1}_{address2}_{zip} {city}_{country}_{phone}"
format_extended:
edit: "{country}_{firstName}{lastName}_{company}_{streetName}{streetNumber}_{address2}_{zip}{city}_{phone}"
address1_regex:
- "^(?<streetName>[^\\d]+) (?<streetNumber>\\d+(?:\\s?[A-za-z])?)$"
additional_address_fields:
- name: streetName
required: true
Expand Down
10 changes: 10 additions & 0 deletions lang/typescript/.changeset/pre.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"mode": "pre",
"tag": "next",
"initialVersions": {
"@shopify/worldwide": "0.6.0"
},
"changesets": [
"split-address1-regex-fallback"
]
}
5 changes: 5 additions & 0 deletions lang/typescript/.changeset/split-address1-regex-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/worldwide': minor
---

Add optional tryRegexFallback param to splitAddress1 function to attempt splitting address lines that do not contain the reserved delimiter
2 changes: 1 addition & 1 deletion lang/typescript/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"selector": "property",
"format": ["strictCamelCase"],
"filter": {
"regex": "^(combined_address_format)$",
"regex": "^(combined_address_format|address1_regex)$",
"match": false
}
}
Expand Down
6 changes: 6 additions & 0 deletions lang/typescript/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @shopify/worldwide

## 0.7.0-next.0

### Minor Changes

- 1f5d405: Add optional tryRegexFallback param to splitAddress1 function to attempt splitting address lines that do not contain the reserved delimiter

## 0.6.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion lang/typescript/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@shopify/worldwide",
"description": "Utilities for parsing and formatting address fields",
"version": "0.6.0",
"version": "0.7.0-next.0",
"repository": "git@github.com:Shopify/worldwide.git",
"author": "Shopify Inc.",
"homepage": "https://github.com/Shopify/worldwide/tree/main/lang/typescript#readme",
Expand Down
7 changes: 6 additions & 1 deletion lang/typescript/rollup-plugin-regions-yaml/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const regionYamlSchema = z.object({
})
.strict()
.optional(),
address1_regex: z.optional(z.array(z.string())),
});
export type RegionYaml = z.infer<typeof regionYamlSchema>;

Expand Down Expand Up @@ -106,7 +107,10 @@ export function validateRegionYaml(
return regionYaml;
}

export type MinimalRegionYaml = Pick<RegionYaml, 'combined_address_format'>;
export type MinimalRegionYaml = Pick<
RegionYaml,
'combined_address_format' | 'address1_regex'
>;

/**
* Strip the YAML data down to only what we need to keep the resulting JS
Expand All @@ -115,5 +119,6 @@ export type MinimalRegionYaml = Pick<RegionYaml, 'combined_address_format'>;
export function transformRegionYaml(regionYaml: RegionYaml): MinimalRegionYaml {
return {
combined_address_format: regionYaml.combined_address_format,
address1_regex: regionYaml.address1_regex,
};
}
107 changes: 97 additions & 10 deletions lang/typescript/src/extended-address/splitAddress1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,122 @@ import {splitAddress1} from './splitAddress1';

describe('splitAddress1', () => {
test('returns null when extended address is not defined for region', () => {
expect(splitAddress1('US', '123 Main')).toBeNull();
expect(splitAddress1('US', '123 Main', false)).toBeNull();
expect(splitAddress1('US', '', false)).toBeNull();
expect(splitAddress1('US', '123 Main', true)).toBeNull();
expect(splitAddress1('US', '', true)).toBeNull();
});

test('returns empty object when extended address string is empty', () => {
expect(splitAddress1('CL', '')).toEqual({});
expect(splitAddress1('BR', '')).toEqual({});
expect(splitAddress1('CL', '', false)).toEqual({});
expect(splitAddress1('BR', '', false)).toEqual({});
expect(splitAddress1('CL', '', true)).toEqual({});
expect(splitAddress1('BR', '', true)).toEqual({});
});

test('returns address1 as street name when no delimiter is present', () => {
expect(splitAddress1('CL', '123 Main')).toEqual({streetName: '123 Main'});
expect(splitAddress1('BR', '123 Main')).toEqual({streetName: '123 Main'});
test('returns address1 as street name when no delimiter is present and tryRegexFallback is false', () => {
expect(splitAddress1('CL', '123 Main', false)).toEqual({
streetName: '123 Main',
});
expect(splitAddress1('BR', '123 Main', false)).toEqual({
streetName: '123 Main',
});
});

test('returns address1 as street name when no delimiter is present, tryRegexFallback is true, and regex is not defined', () => {
expect(splitAddress1('CL', '123 Main', true)).toEqual({
streetName: '123 Main',
});
expect(splitAddress1('BR', '123 Main', true)).toEqual({
streetName: '123 Main',
});
});

test.each([
{
country: 'NL',
address: 'Kempenaar 25 11',
expected: {streetName: 'Kempenaar 25 11'},
},
{
country: 'NL',
address: '40 Baandersstraat',
expected: {streetName: '40 Baandersstraat'},
},
{
country: 'BR',
address: 'Main, 123, Apt 2',
expected: {streetName: 'Main, 123, Apt 2'},
},
])(
'returns address1 as street name when no delimiter is present, tryRegexFallback is true, and address does not match regex',
({country, address, expected}) => {
expect(splitAddress1(country, address, true)).toEqual(expected);
},
);

test('returns street number if string before delimiter is empty', () => {
expect(splitAddress1('CL', '\u2060123')).toEqual({streetNumber: '123'});
expect(splitAddress1('BR', '\u2060123')).toEqual({streetNumber: '123'});
expect(splitAddress1('CL', '\u2060123', false)).toEqual({
streetNumber: '123',
});
expect(splitAddress1('BR', '\u2060123', false)).toEqual({
streetNumber: '123',
});
expect(splitAddress1('CL', '\u2060123', true)).toEqual({
streetNumber: '123',
});
expect(splitAddress1('BR', '\u2060123', true)).toEqual({
streetNumber: '123',
});
});

test('returns full address object when separated by delimiter', () => {
expect(splitAddress1('CL', 'Main \u2060123')).toEqual({
expect(splitAddress1('CL', 'Main \u2060123', false)).toEqual({
streetName: 'Main',
streetNumber: '123',
});
expect(splitAddress1('CL', 'Main \u2060123', true)).toEqual({
streetName: 'Main',
streetNumber: '123',
});
});

test('returns full address object when separated by delimiter and decorator', () => {
expect(splitAddress1('BR', 'Main, \u2060123')).toEqual({
expect(splitAddress1('BR', 'Main, \u2060123', false)).toEqual({
streetName: 'Main',
streetNumber: '123',
});
expect(splitAddress1('BR', 'Main, \u2060123', true)).toEqual({
streetName: 'Main',
streetNumber: '123',
});
});

test.each([
{
country: 'NL',
address: 'Mercuriusstraat 26',
expected: {streetName: 'Mercuriusstraat', streetNumber: '26'},
},
{
country: 'NL',
address: 'Bloemgracht 41B',
expected: {streetName: 'Bloemgracht', streetNumber: '41B'},
},
{
country: 'NL',
address: 'Bloemgracht 41b',
expected: {streetName: 'Bloemgracht', streetNumber: '41b'},
},
{
country: 'NL',
address: 'Meester Arendstraat 48 B',
expected: {streetName: 'Meester Arendstraat', streetNumber: '48 B'},
},
])(
'returns full address object when not separated by delimiter, tryRegexFallback is true and address matches regex',
({country, address, expected}) => {
expect(splitAddress1(country, address, true)).toEqual(expected);
},
);
});
48 changes: 38 additions & 10 deletions lang/typescript/src/extended-address/splitAddress1.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,54 @@
import type {Address} from '../types/address';
import {splitAddressField} from '../utils/address-fields';
import {getRegionConfig, getConcatenationRules} from '../utils/regions';
import {
RESERVED_DELIMITER,
splitAddressField,
regexSplitAddressField,
} from '../utils/address-fields';
import {
getRegionConfig,
getConcatenationRules,
getAddress1Regex,
} from '../utils/regions';

/**
* Parse a concatenated address1 string based on the region specified by
* country code
* Splits an address string into sub-fields using a reserved delimiter.
* Optionally provides a fallback mechanism to parse the address using
* a regex when the delimiter is absent, which should be used with caution
* as it may not provide accurate results.
*
* @param countryCode 2-letter country code string
* @param concatenatedAddress Combined address1 string
* @param address Combined address1 string
* @param tryRegexFallback Flag to attempt regex parsing as a fallback mechanism
* @returns Partial Address object containing parsed address fields or null if
* the region does not define an extended address format
*/
export function splitAddress1(
countryCode: string,
concatenatedAddress: string,
address: string,
tryRegexFallback = false,
): Partial<Address> | null {
const config = getRegionConfig(countryCode);
const fieldConcatenationRules = config
? getConcatenationRules(config, concatenatedAddress, 'address1')
? getConcatenationRules(config, address, 'address1')
: undefined;
if (fieldConcatenationRules) {
return splitAddressField(fieldConcatenationRules, concatenatedAddress);
const address1Regex = config ? getAddress1Regex(config) : undefined;
if (!fieldConcatenationRules) {
return null;
}

return null;
if (address === '') {
return {};
}

if (address.includes(RESERVED_DELIMITER)) {
return splitAddressField(fieldConcatenationRules, address);
}
if (tryRegexFallback && address1Regex) {
return regexSplitAddressField(
fieldConcatenationRules,
address1Regex,
address,
);
}
return {[fieldConcatenationRules[0].key]: address};
}
2 changes: 2 additions & 0 deletions lang/typescript/src/types/region-yaml-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ export type FieldDefinitions = Record<
export type RegionYamlConfig = Record<string, any> & {
/** Format definition for an extended address */
combined_address_format?: CombinedAddressFormat;
/** Regex patterns for standard address1 */
address1_regex?: string[];
};
91 changes: 91 additions & 0 deletions lang/typescript/src/utils/address-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {FieldConcatenationRule} from 'src/types/region-yaml-config';
import {
RESERVED_DELIMITER,
concatAddressField,
regexSplitAddressField,
splitAddressField,
} from './address-fields';

Expand Down Expand Up @@ -214,4 +215,94 @@ describe('splitAddressField', () => {
});
});
});

describe('regexSplitAddressField', () => {
test('creates an address object from string matching one of the defined regexes', () => {
const fieldDefinition: FieldConcatenationRule[] = [
{key: 'streetName'},
{key: 'streetNumber'},
];
const regexPatterns = [
new RegExp('^(?<streetNumber>\\d+) (?<streetName>[^\\d]+)$'),
new RegExp('^(?<streetName>[^\\d]+) (?<streetNumber>\\d+)$'),
];
const address = 'Main 123';

expect(
regexSplitAddressField(fieldDefinition, regexPatterns, address),
).toEqual({
streetName: 'Main',
streetNumber: '123',
});
});

test('creates an address object from string matching multiple of the defined regexes', () => {
const fieldDefinition: FieldConcatenationRule[] = [
{key: 'streetName'},
{key: 'streetNumber'},
];
const regexPatterns = [
new RegExp('^(?<streetName>[^\\d]+) (?<streetNumber>\\d+)$'),
new RegExp('^(?<dupName>[^\\d]+) (?<dupNumber>\\d+)$'),
];
const address = 'Main 123';

expect(
regexSplitAddressField(fieldDefinition, regexPatterns, address),
).toEqual({
streetName: 'Main',
streetNumber: '123',
});
});

test('creates a partial address object from string that does not match one of the defined regexes', () => {
const fieldDefinition: FieldConcatenationRule[] = [
{key: 'streetName'},
{key: 'streetNumber'},
];
const regexPatterns = [
new RegExp('^(?<streetNumber>\\d+) (?<streetName>[^\\d]+)$'),
];
const address = 'Main 123';

expect(
regexSplitAddressField(fieldDefinition, regexPatterns, address),
).toEqual({
streetName: 'Main 123',
});
});

test('field definition order matters', () => {
const fieldDefinitionNumberFirst: FieldConcatenationRule[] = [
{key: 'streetNumber'},
{key: 'streetName'},
];
const fieldDefinitionNameFirst: FieldConcatenationRule[] = [
{key: 'streetName'},
{key: 'streetNumber'},
];
const regexPatterns = [
new RegExp('^(?<streetName>[^\\d]+) (?<streetNumber>\\d+)$'),
];
const address = 'Main';
expect(
regexSplitAddressField(
fieldDefinitionNumberFirst,
regexPatterns,
address,
),
).toEqual({
streetNumber: 'Main',
});
expect(
regexSplitAddressField(
fieldDefinitionNameFirst,
regexPatterns,
address,
),
).toEqual({
streetName: 'Main',
});
});
});
});
Loading

0 comments on commit 7829478

Please sign in to comment.