diff --git a/packages/format/README.md b/packages/format/README.md index e7f2cb4..aefd172 100644 --- a/packages/format/README.md +++ b/packages/format/README.md @@ -36,5 +36,6 @@ formatOrganizationNumber('0000000000') // => '000000-0000' ## Methods +* formatPhoneNumber * formatOrganizationNumber * formatObosMembershipNumber diff --git a/packages/format/src/format.test.ts b/packages/format/src/format.test.ts index b470766..c5650d8 100644 --- a/packages/format/src/format.test.ts +++ b/packages/format/src/format.test.ts @@ -2,13 +2,22 @@ import { describe, expect, test } from 'vitest'; import { formatObosMembershipNumber as formatObosMembershipNumberNo, formatOrganizationNumber as formatOrganizationNumberNo, + formatPhoneNumber as formatPhoneNumberNo, } from './no'; import { formatObosMembershipNumber as formatObosMembershipNumberSe, formatOrganizationNumber as formatOrganizationNumberSe, + formatPhoneNumber as formatPhoneNumberSe, } from './se'; describe('no', () => { + test.each([ + ['22865500', '22 86 55 00'], + ['80000000', '800 00 000'], + ])('formatPhoneNumber(%s) -> %s', (input, expected) => { + expect(formatPhoneNumberNo(input)).toBe(expected); + }); + test.each([ ['000000000', '000 000 000'], ['000 000 000', '000 000 000'], @@ -20,6 +29,31 @@ describe('no', () => { }); describe('se', () => { + test.each([ + // mobile phone numbers + ['0701234567', '070-123 45 67'], + ['070 12 345 67', '070-123 45 67'], + // 2 digit area code + ['0812345', '08-123 45'], + ['08123456', '08-12 34 56'], + ['081234567', '08-123 45 67'], + ['0812345678', '08-123 456 78'], + // 3 digit area code + ['03112345', '031-123 45'], + ['031123456', '031-12 34 56'], + ['0311234567', '031-123 45 67'], + ['03112345678', '031-123 456 78'], + // 4 digit area code + ['030312345', '0303-123 45'], + ['0303123456', '0303-12 34 56'], + ['03031234567', '0303-123 45 67'], + ['030312345678', '0303-123 456 78'], + // invalid, too long a number + ['0303123456789', '0303123456789'], + ])('formatPhoneNumber(%s) -> %s', (input, expected) => { + expect(formatPhoneNumberSe(input)).toBe(expected); + }); + test.each([ ['0000000000', '000000-0000'], ['000000-0000', '000000-0000'], diff --git a/packages/format/src/no.ts b/packages/format/src/no.ts index bd59b6f..b53be2d 100644 --- a/packages/format/src/no.ts +++ b/packages/format/src/no.ts @@ -1,5 +1,36 @@ import { replaceIfMatch } from './utils'; +// Regular phone number format is: 00 00 00 00 +// if the number starts with 8, it's an 800-series number, with the format: 800 00 000 +// See https://sprakradet.no/godt-og-korrekt-sprak/rettskriving-og-grammatikk/tall-tid-dato/ +const REGULAR_PHONE_NUMBER_FORMAT = /^(\d{2})(\d{2})(\d{2})(\d{2})$/; +const EIGHT_HUNDRED_SERIES_PHONE_NUMBER_FORMAT = /^(\d{3})(\d{2})(\d{3})$/; + +/** + * Format a phone number + * @example + * ``` + * formatPhoneNumber('00000000') // => '00 00 00 00' + * formatPhoneNumber('80000000') // => '800 00 000' + * ``` + */ +export function formatPhoneNumber(input: string): string { + const number = replaceIfMatch( + input, + REGULAR_PHONE_NUMBER_FORMAT, + '$1 $2 $3 $4', + ); + + // if the number starts with 8, it's an 800-series number, so we'll format it differently + return number.startsWith('8') + ? replaceIfMatch( + number, + EIGHT_HUNDRED_SERIES_PHONE_NUMBER_FORMAT, + '$1 $2 $3', + ) + : number; +} + const ORG_NUMBER_FORMAT = /^(\d{3})(\d{3})(\d{3})$/; /** diff --git a/packages/format/src/se.ts b/packages/format/src/se.ts index be1f380..be33c8c 100644 --- a/packages/format/src/se.ts +++ b/packages/format/src/se.ts @@ -1,4 +1,59 @@ -import { replaceIfMatch } from './utils'; +import { cleanInput, replaceIfMatch } from './utils'; + +const MOBILE_PHONE_NUMBER_FORMAT = /^(07[02369]{1})(\d{3})(\d{2})(\d{2})$/; +// subscriber numbers, without the area codes, can be 5, 6, 7 or 8 digits long +const SUBSCRIBER_NUMBER_FORMATS = { + 5: /^(\d{3})(\d{2})$/, + 6: /^(\d{2})(\d{2})(\d{2})$/, + 7: /^(\d{3})(\d{2})(\d{2})$/, + 8: /^(\d{3})(\d{3})(\d{2})$/, +}; +// SE numbers have area codes of 2, 3 or 4 digits +const TWO_DIGIT_AREA_CODE = /^08/; +const THREE_DIGIT_AREA_CODE = + /^0(11|13|16|18|19|21|23|26|31|33|35|36|40|42|44|46|54|60|63|90)/; + +/** + * Format a phone number + * @example + * ``` + * formatPhoneNumber('07012345678') // => '070-123 45 678' + * formatPhoneNumber('0812345') // => '08-123 45' + * formatPhoneNumber('0311234567') // => '031-123 45 67' + * formatPhoneNumber('0303123456') // => '0303-12 34 56' + * ``` + */ +export function formatPhoneNumber(input: string): string { + const normalizedInput = cleanInput(input); + + if (MOBILE_PHONE_NUMBER_FORMAT.test(normalizedInput)) { + return normalizedInput.replace(MOBILE_PHONE_NUMBER_FORMAT, '$1-$2 $3 $4'); + } + + const areaCodeLength = TWO_DIGIT_AREA_CODE.test(normalizedInput) + ? 2 + : THREE_DIGIT_AREA_CODE.test(normalizedInput) + ? 3 + : 4; + + const areaCode = normalizedInput.substring(0, areaCodeLength); + const subscriberNumber = normalizedInput.substring(areaCodeLength); + + // if the subscriber number length is not in the formats, return the input as is + if (!(subscriberNumber.length in SUBSCRIBER_NUMBER_FORMATS)) { + return normalizedInput; + } + + const subscriberNumberFormat = + SUBSCRIBER_NUMBER_FORMATS[ + // the cast should be okay here, as we've checked the length above + subscriberNumber.length as keyof typeof SUBSCRIBER_NUMBER_FORMATS + ]; + + const replacePattern = subscriberNumber.length === 5 ? '$1 $2' : '$1 $2 $3'; + + return `${areaCode}-${subscriberNumber.replace(subscriberNumberFormat, replacePattern)}`; +} const ORG_NUMBER_FORMAT = /^(\d{6})(\d{4})$/; diff --git a/packages/format/src/utils.ts b/packages/format/src/utils.ts index 80e96ee..58373b5 100644 --- a/packages/format/src/utils.ts +++ b/packages/format/src/utils.ts @@ -3,10 +3,14 @@ export function replaceIfMatch( regex: RegExp, replacerPattern: string, ): string { + const normalizedInput = cleanInput(input); + + return normalizedInput.replace(regex, replacerPattern); +} + +export function cleanInput(input: string): string { // We're extremely lenient when attemtping to format the input. // We remove everything that isn't a letter or a number, that way we can get rid of any // formatting that might already be present in the input, eg spaces, hyphens or dots - const cleaned = input.replace(/[^a-zA-Z0-9]/g, ''); - - return cleaned.replace(regex, replacerPattern); + return input.replace(/[^a-zA-Z0-9]/g, ''); }