import { Destination, OriginAddress } from 'types';
/**
* Returns true if there are no origin addresses or every address is unverified.
*
* @param originAddresses - Array of origin addresses (e.g. from getOriginAddresses())
* @return true when no addresses exist or all have isVerified falsy
*/
export const areAllOriginsUnverified = (
originAddresses: OriginAddress[] | undefined | null
): boolean =>
! originAddresses?.length ||
originAddresses.every(
( address ) =>
address.isApproved === false ||
( ( address.isApproved === undefined ||
address.isApproved === null ) &&
! address.isVerified )
);
/**
* Map of common address abbreviations to their full names.
* We only use this to ensure that leveinstein distance is not affected by abbreviations.
*/
export const addresAbbreviationMap: Record< string, string > = {
STREET: 'ST',
AVENUE: 'AVE',
BOULEVARD: 'BLVD',
ROAD: 'RD',
DRIVE: 'DR',
LANE: 'LN',
COURT: 'CT',
PARKWAY: 'PKWY',
PLACE: 'PL',
TERRACE: 'TER',
CIRCLE: 'CIR',
HIGHWAY: 'HWY',
MOUNT: 'MT',
MOUNTAIN: 'MTN',
SQUARE: 'SQ',
SUITE: 'STE',
BUILDING: 'BLDG',
FLOOR: 'FL',
ROOM: 'RM',
APARTMENT: 'APT',
UNIT: 'UNIT',
HARBOR: 'HBR',
ISLAND: 'IS',
CREEK: 'CRK',
HEIGHTS: 'HTS',
SPRING: 'SPG',
VALLEY: 'VLY',
CROSSING: 'XING',
NORTH: 'N',
SOUTH: 'S',
EAST: 'E',
WEST: 'W',
// Add other common abbreviations as needed
};
/**
* Normalise an address by removing periods, commas, extra spaces, and converting to uppercase.
*
* @param address The address to normalise
*
* @return The normalised address
*/
export const normaliseAddress = ( address: string ): string => {
let normalised = address
.toUpperCase() // Convert to uppercase
.replace( /\./g, '' ) // Remove periods
.replace( /,/g, '' ) // Remove commas
.replace( /\s+/g, ' ' ); // Remove extra spaces
// Replace full words with abbreviations.
Object.keys( addresAbbreviationMap ).forEach( ( abbr ) => {
const regex = new RegExp( `\\b${ abbr }\\b`, 'g' ); // Match the abbreviation as a whole word
normalised = normalised.replace( regex, addresAbbreviationMap[ abbr ] );
} );
return normalised;
};
/**
* Compare two addresses and return the Levenshtein distance between them to determine similarity.
*
* @param address1 The first address to compare
* @param address2 The second address to compare
*
* @return number
*/
export const levenshteinDistance = (
address1: string,
address2: string
): number => {
const matrix: number[][] = [];
// Increment along the first column of each row
for ( let i = 0; i <= address2.length; i++ ) {
matrix[ i ] = [ i ];
}
// Increment each column in the first row
for ( let j = 0; j <= address1.length; j++ ) {
matrix[ 0 ][ j ] = j;
}
// Fill the matrix
for ( let i = 1; i <= address2.length; i++ ) {
for ( let j = 1; j <= address1.length; j++ ) {
if ( address2.charAt( i - 1 ) === address1.charAt( j - 1 ) ) {
matrix[ i ][ j ] = matrix[ i - 1 ][ j - 1 ];
} else {
matrix[ i ][ j ] = Math.min(
matrix[ i - 1 ][ j - 1 ] + 1,
Math.min(
matrix[ i ][ j - 1 ] + 1,
matrix[ i - 1 ][ j ] + 1
)
);
}
}
}
return matrix[ address2.length ][ address1.length ];
};
/**
* Compare two addresses to determine if they are similar.
* @param address1 The first address to compare
* @param address2 The second address to compare
* @return boolean True if the addresses are similar, false otherwise
*/
export const areAddressesClose = (
address1: Destination,
address2: Destination
): boolean => {
const normStreet1Address1 = address1.address1
? normaliseAddress( address1.address1 )
: '';
const normStreet1Address2 = address2.address1
? normaliseAddress( address2.address1 )
: '';
const normStreet2Address1 = address1.address2
? normaliseAddress( address1.address2 )
: '';
const normStreet2Address2 = address2.address2
? normaliseAddress( address2.address2 )
: '';
const normCityAddress1 = address1.city?.toUpperCase() ?? '';
const normCityAddress2 = address2.city?.toUpperCase() ?? '';
const normStateAddress1 = address1.state?.toUpperCase() ?? '';
const normStateAddress2 = address2.state?.toUpperCase() ?? '';
// Extract the base 5 digits of the ZIP code to account for ZIP+4 codes
const normPostalCodeAddress1 = address1.postcode.slice( 0, 5 ); // First 5 digits
const normPostalCodeAddress2 = address2.postcode.slice( 0, 5 ); // First 5 digits
const normCountryAddress1 = address1.country.toUpperCase();
const normCountryAddress2 = address2.country.toUpperCase();
// Set thresholds (adjust as necessary)
const STREET_THRESHOLD = 5; // Maximum number of character changes allowed for street
// Compare each aspect of the addresses
const isStreetSimilar =
levenshteinDistance( normStreet1Address1, normStreet1Address2 ) <=
STREET_THRESHOLD;
const isStreet2Similar =
levenshteinDistance( normStreet2Address1, normStreet2Address2 ) <=
STREET_THRESHOLD;
const isCitySimilar = normCityAddress1 === normCityAddress2;
const isStateSimilar = normStateAddress1 === normStateAddress2;
const isPostalCodeSimilar =
normPostalCodeAddress1 === normPostalCodeAddress2; // Compare only the first 5 digits
const isCountrySimilar = normCountryAddress1 === normCountryAddress2;
// Return true if all key address parts are considered "similar"
return (
isStreetSimilar &&
isStreet2Similar &&
isCitySimilar &&
isStateSimilar &&
isPostalCodeSimilar &&
isCountrySimilar
);
};
/**
* Check if only checkbox fields (defaultAddress, defaultReturnAddress) have changed
* and no actual address data fields have been modified.
*
* @param originalAddress The original address object
* @param newAddress The new address object with potentially changed values
* @return boolean True if only checkbox fields changed, false otherwise
*/
export const hasOnlyCheckboxChanges = < T extends Record< string, unknown > >(
originalAddress: T,
newAddress: T
): boolean => {
const addressFields = [
'name',
'company',
'address',
'city',
'state',
'postcode',
'country',
'email',
'phone',
'firstName',
'lastName',
];
// Check if any actual address fields have changed
const hasAddressFieldChanges = addressFields.some( ( field ) => {
const originalValue = originalAddress[ field as keyof T ];
const newValue = newAddress[ field as keyof T ];
// Type-agnostic comparison to handle cases like 1 vs '1'
// eslint-disable-next-line eqeqeq
return originalValue != newValue;
} );
return ! hasAddressFieldChanges;
};