Skip to content

Commit

Permalink
Merge pull request #193 from Shopify/additional-address-split-ts
Browse files Browse the repository at this point in the history
Additional address split in TypeScript
  • Loading branch information
kennygoff authored Jun 5, 2024
2 parents 8e44f14 + 681a584 commit ccd2596
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 2 deletions.
5 changes: 5 additions & 0 deletions lang/typescript/.changeset/clean-needles-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/worldwide': minor
---

Add parsing functions `splitAddress1` and `splitAddress2`
28 changes: 28 additions & 0 deletions lang/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,34 @@ concatenateAddress2({
}); // returns 'Apt 2'
```

### Parsing a concatentated address string

To parse a concatenated address string use the split functions which return a
partial Address object including any address fields we are able to match given
the region specified.

Using our Brazil example, we can pass the concatenated string into our split
function for address1:

```ts
splitAddress1('BR', 'Av. Paulista,\u00A0 1578'); // returns { streetName: 'Av. Paulista', streetNumber: '1578' }
```

Trying to parse an address string for a region that doesn't have a defined
`combined_address_format` will return `null`.

```ts
import {splitAddress1, splitAddress2} from '@shopify/worldwide';
// Parse Address1
splitAddress1('BR', 'Av. Paulista,\u00A0 1578'); // returns { streetName: 'Av. Paulista', streetNumber: '1578' }
splitAddress1('US', '123 Main'); // returns null
// Parse Address2
splitAddress2('BR', 'dpto 4,\u00A0Centro'); // returns { line2: 'dpto 4', neighborhood: 'Centro', }
splitAddress2('US', 'Apt 2'); // returns null
```

## Contributing & Development

### Setup
Expand Down
2 changes: 2 additions & 0 deletions lang/typescript/src/extended-address/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export {concatenateAddress1} from './concatenateAddress1';
export {concatenateAddress2} from './concatenateAddress2';
export {splitAddress1} from './splitAddress1';
export {splitAddress2} from './splitAddress2';
36 changes: 36 additions & 0 deletions lang/typescript/src/extended-address/splitAddress1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {splitAddress1} from './splitAddress1';

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

test('returns empty object when extended address string is empty', () => {
expect(splitAddress1('CL', '')).toEqual({});
expect(splitAddress1('BR', '')).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 street number if string before delimiter is empty', () => {
expect(splitAddress1('CL', '\u00A0123')).toEqual({streetNumber: '123'});
expect(splitAddress1('BR', '\u00A0123')).toEqual({streetNumber: '123'});
});

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

test('returns full address object when separated by delimiter and decorator', () => {
expect(splitAddress1('BR', 'Main,\u00A0123')).toEqual({
streetName: 'Main',
streetNumber: '123',
});
});
});
25 changes: 25 additions & 0 deletions lang/typescript/src/extended-address/splitAddress1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {Address} from '../types/address';
import {splitAddressField} from '../utils/address-fields';
import {getRegionConfig} from '../utils/regions';

/**
* Parse a concatenated address1 string based on the region specified by
* country code
* @param countryCode 2-letter country code string
* @param concatenatedAddress Combined address1 string
* @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,
): Partial<Address> | null {
const config = getRegionConfig(countryCode);
const address1CombinedFormat = config?.combined_address_format?.address1;

if (address1CombinedFormat) {
return splitAddressField(address1CombinedFormat, concatenatedAddress);
}

return null;
}
40 changes: 40 additions & 0 deletions lang/typescript/src/extended-address/splitAddress2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {splitAddress2} from './splitAddress2';

describe('splitAddress2', () => {
test('returns null when extended address is not defined for region', () => {
expect(splitAddress2('US', '#2, Centretown')).toBeNull();
});

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

test('returns address2 as line2 when no delimiter is present', () => {
expect(splitAddress2('CL', 'dpto 4')).toEqual({line2: 'dpto 4'});
expect(splitAddress2('BR', 'dpto 4')).toEqual({line2: 'dpto 4'});
});

test('returns neighborhood if string before delimiter is empty', () => {
expect(splitAddress2('CL', '\u00A0Centro')).toEqual({
neighborhood: 'Centro',
});
expect(splitAddress2('BR', '\u00A0Centro')).toEqual({
neighborhood: 'Centro',
});
});

test('returns full address object when separated by delimiter', () => {
expect(splitAddress2('CL', 'dpto 4\u00A0Centro')).toEqual({
line2: 'dpto 4',
neighborhood: 'Centro',
});
});

test('returns full address object when separated by delimiter and decorator', () => {
expect(splitAddress2('BR', 'dpto 4,\u00A0Centro')).toEqual({
line2: 'dpto 4',
neighborhood: 'Centro',
});
});
});
25 changes: 25 additions & 0 deletions lang/typescript/src/extended-address/splitAddress2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {Address} from '../types/address';
import {splitAddressField} from '../utils/address-fields';
import {getRegionConfig} from '../utils/regions';

/**
* Parse a concatenated address2 string based on the region specified by
* country code
* @param countryCode 2-letter country code string
* @param concatenatedAddress Combined address2 string
* @returns Partial Address object containing parsed address fields or null if
* the region does not define an extended address format
*/
export function splitAddress2(
countryCode: string,
concatenatedAddress: string,
): Partial<Address> | null {
const config = getRegionConfig(countryCode);
const address2CombinedFormat = config?.combined_address_format?.address2;

if (address2CombinedFormat) {
return splitAddressField(address2CombinedFormat, concatenatedAddress);
}

return null;
}
7 changes: 6 additions & 1 deletion lang/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export {RESERVED_DELIMITER} from './utils/address-fields';
export {concatenateAddress1, concatenateAddress2} from './extended-address';
export {
concatenateAddress1,
concatenateAddress2,
splitAddress1,
splitAddress2,
} from './extended-address';
95 changes: 94 additions & 1 deletion lang/typescript/src/utils/address-fields.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import {FieldConcatenationRule} from './regions';
import {RESERVED_DELIMITER, concatAddressField} from './address-fields';
import {
RESERVED_DELIMITER,
concatAddressField,
splitAddressField,
} from './address-fields';

describe('RESERVED_DELIMITER', () => {
test('is a non-breaking space', () => {
Expand Down Expand Up @@ -110,3 +114,92 @@ describe('concatAddressField', () => {
});
});
});

describe('splitAddressField', () => {
test('creates an address object from string with reserved delimiter', () => {
const fieldDefinition: FieldConcatenationRule[] = [
{key: 'streetNumber'},
{key: 'streetName'},
];
const concatenatedAddress = '123\u00A0Main';

expect(splitAddressField(fieldDefinition, concatenatedAddress)).toEqual({
streetNumber: '123',
streetName: 'Main',
});
});

test('field definition order matters', () => {
const fieldDefinitionNumberFirst: FieldConcatenationRule[] = [
{key: 'streetNumber'},
{key: 'streetName'},
];
const fieldDefinitionNameFirst: FieldConcatenationRule[] = [
{key: 'streetName'},
{key: 'streetNumber'},
];
const concatenatedAddress = '123\u00A0Main';
expect(
splitAddressField(fieldDefinitionNumberFirst, concatenatedAddress),
).toEqual({
streetNumber: '123',
streetName: 'Main',
});
expect(
splitAddressField(fieldDefinitionNameFirst, concatenatedAddress),
).toEqual({
streetName: '123',
streetNumber: 'Main',
});
});

test('creates an address object from string with partial data', () => {
const fieldDefinition: FieldConcatenationRule[] = [
{key: 'streetNumber'},
{key: 'streetName'},
];
expect(splitAddressField(fieldDefinition, '123')).toEqual({
streetNumber: '123',
});
expect(splitAddressField(fieldDefinition, '\u00A0Main')).toEqual({
streetName: 'Main',
});
});

describe('fields with decorators', () => {
test('splits address with decorator', () => {
const fieldDefinition: FieldConcatenationRule[] = [
{key: 'streetName'},
{key: 'streetNumber', decorator: ','},
];
const concatenatedAddress = 'Main,\u00A0123';

expect(splitAddressField(fieldDefinition, concatenatedAddress)).toEqual({
streetName: 'Main',
streetNumber: '123',
});
});
test('splits address without defined decorator', () => {
const fieldDefinition: FieldConcatenationRule[] = [
{key: 'streetName'},
{key: 'streetNumber', decorator: ','},
];
const concatenatedAddress = 'Main';

expect(splitAddressField(fieldDefinition, concatenatedAddress)).toEqual({
streetName: 'Main',
});
});
test('splits address without defined decorator 2', () => {
const fieldDefinition: FieldConcatenationRule[] = [
{key: 'streetName'},
{key: 'streetNumber', decorator: ','},
];
const concatenatedAddress = '\u00A0123';

expect(splitAddressField(fieldDefinition, concatenatedAddress)).toEqual({
streetNumber: '123',
});
});
});
});
37 changes: 37 additions & 0 deletions lang/typescript/src/utils/address-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,40 @@ export function concatAddressField(
return concatenatedAddress;
}, '');
}

/**
* Utility function that parses an address string based on a provided field
* definition and set of values for those fields
*
* @param fieldDefinition Array of definitions of address sub-fields
* @param concatenatedAddress Concatenated string of address field
* @returns Partial Address object of fields parsed from string
*/
export function splitAddressField(
fieldDefinition: FieldConcatenationRule[],
concatenatedAddress: string,
): Partial<Address> {
const values = concatenatedAddress.split(RESERVED_DELIMITER);

const parsedAddressObject = values.reduce((obj, value, index) => {
if (value !== '') {
// Decorator is included as a suffix in the previous sub-field value,
// so we need to strip it from the current field by looking ahead at the
// next field's definition
// Ex: streetNumber decorator is ","; ["Main,", "123"] => ["Main", "123"]
const nextFieldDecorator = fieldDefinition[index + 1]?.decorator;
const fieldValue =
nextFieldDecorator && value.endsWith(nextFieldDecorator)
? value.substring(0, value.length - nextFieldDecorator.length)
: value;
return {
...obj,
[fieldDefinition[index].key]: fieldValue,
};
}

return obj;
}, {});

return parsedAddressObject;
}

0 comments on commit ccd2596

Please sign in to comment.