<?php
/**
* AddressUtils class.
*
* A class to provide utility methods for working with addresses.
*
* @package Automattic/WCShipping
*/
namespace Automattic\WCShipping\Utilities;
use Automattic\WooCommerce\StoreApi\Utilities\ValidationUtils;
/**
* AddressUtils class.
*/
class AddressUtils {
/**
* Standard address fields.
*
* @var array
*/
public static array $standard_address_fields = array(
'address_1',
'address_2',
'city',
'state',
'postcode',
'country',
);
/**
* Map of address abbreviations.
*
* @var array
*/
public static array $address_abbreviation_map = array(
'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',
);
/**
* Compare two addresses to determine if they are close.
*
* @param array $address1 The first address.
* @param array $address2 The second address.
*
* @return bool
*/
public static function are_addresses_close( array $address1, array $address2 ): bool {
$address1 = self::get_standardized_address( $address1 );
$address2 = self::get_standardized_address( $address2 );
// Set thresholds (adjust as necessary)
$STREET_THRESHOLD = 5;
foreach ( self::$standard_address_fields as $field ) {
if ( in_array( $field, array( 'address_1', 'address_2' ), true ) ) {
if ( levenshtein( $address1[ $field ], $address2[ $field ] ) > $STREET_THRESHOLD ) {
return false;
}
} elseif ( $address1[ $field ] !== $address2[ $field ] ) {
return false;
}
}
return true;
}
/**
* Returns a standardized address string.
*
* @param array $address The address.
*
* @return array
*/
public static function get_standardized_address( array $address ): array {
$standardized_address = array();
foreach ( self::$standard_address_fields as $field ) {
if ( empty( $address[ $field ] ) ) {
$standardized_address[ $field ] = '';
} elseif ( in_array( $field, array( 'city', 'country' ), true ) ) {
$standardized_address[ $field ] = strtoupper( $address[ $field ] );
} elseif ( 'state' === $field ) {
$standardized_address[ $field ] = self::convert_state_to_wc_format( $address[ $field ], $address['country'] );
} elseif ( 'postcode' === $field ) {
$standardized_address[ $field ] = wc_format_postcode( $address[ $field ], $address['country'] );
} else {
$standardized_address[ $field ] = self::get_standardized_address_field( $address[ $field ] );
}
}
return $standardized_address;
}
/**
* Convert a state to WooCommerce format.
*
* @param string $state The state.
* @param string $country The country.
*
* @return string
*/
public static function convert_state_to_wc_format( string $state, string $country ): string {
$wc_validation_utils = new ValidationUtils();
$state = wc_strtoupper( remove_accents( $state ) );
$country = wc_strtoupper( $country );
$states = $wc_validation_utils->get_states_for_country( $country );
$state_codes = array_map( 'wc_strtoupper', array_keys( $states ) );
// Check if the state is already in the correct format.
if ( in_array( $state, $state_codes, true ) ) {
return $state;
}
$country_state_combo = $country . '-' . $state;
// Check if the state is a country-state combo.
if ( in_array( $country_state_combo, $state_codes, true ) ) {
return $country_state_combo;
}
// Check if the state is a full name.
if ( count( $states ) ) {
$standardized_states = array_map(
'wc_strtoupper',
array_map(
'remove_accents',
$states
)
);
$state_values = array_map( 'wc_strtoupper', array_flip( array_map( 'wc_strtoupper', $standardized_states ) ) );
if ( isset( $state_values[ $state ] ) ) {
// Convert to state code if a state name was provided.
return $state_values[ $state ];
}
}
return $state;
}
/**
* Get standardized address field.
*
* @param string $field The address field.
*
* @return string
*/
public static function get_standardized_address_field( string $field ): string {
// Convert to uppercase and remove periods, commas, and extra spaces.
$result = strtoupper( $field );
$result = preg_replace( '/\./', '', $result );
$result = preg_replace( '/,/', '', $result );
$result = preg_replace( '/\s+/', ' ', $result );
// Replace full words with abbreviations.
foreach ( self::$address_abbreviation_map as $full => $abbr ) {
$result = preg_replace( '/\b' . preg_quote( $full, '/' ) . '\b/', $abbr, $result );
}
return $result;
}
/**
* Compare two addresses to determine if they are equal.
*
* @param array $address1 The first address.
* @param array $address2 The second address.
*
* @return bool
*/
public static function are_addresses_equal( array $address1, array $address2 ): bool {
$address1 = array_map( 'strtoupper', $address1 );
$address2 = array_map( 'strtoupper', $address2 );
// Sort the address fields.
$address1 = self::get_standardized_address( $address1 );
$address2 = self::get_standardized_address( $address2 );
return $address1 === $address2;
}
/**
* Get required shipping address field keys for the passed country.
*
* @param string $country The country.
* @param string $type The address type. Default is 'billing_'.
*
* @return array
*/
public static function get_required_shipping_address_field_keys( string $country, string $type = 'billing_' ): array {
$fields_for_country = self::get_country_address_fields( $country, $type );
$locale = WC()->countries->get_country_locale();
$country_locale = $locale[ $country ] ?? array();
$field_keys_to_ignore = array( 'first_name', 'last_name', 'company', 'email', 'phone' );
$required_field_keys = array();
foreach ( $fields_for_country as $field_key => $field ) {
$key = str_replace( $type, '', $field_key );
if ( in_array( $key, $field_keys_to_ignore, true ) ) {
continue;
}
$required = $field['required'] ?? $country_locale[ $key ]['required'] ?? false;
if ( $required ) {
$required_field_keys[] = $key;
}
}
return $required_field_keys;
}
/**
* Get address field keys to validate for the passed country.
*
* @param string $country The country.
* @param string $type The address type. Default is 'billing_'.
*
* @return array
*/
public static function get_address_field_keys_to_validate( string $country, string $type = 'billing_' ): array {
$fields_for_country = self::get_country_address_fields( $country, $type );
$locale = WC()->countries->get_country_locale();
$country_locale = $locale[ $country ] ?? array();
$field_keys_to_ignore = array( 'first_name', 'last_name', 'company', 'email', 'phone' );
$field_keys_to_validate = array();
foreach ( $fields_for_country as $field_key => $field ) {
$key = str_replace( $type, '', $field_key );
if ( in_array( $key, $field_keys_to_ignore, true ) ) {
continue;
}
$hidden = $field['hidden'] ?? $country_locale[ $key ]['hidden'] ?? false;
$required = $field['required'] ?? $country_locale[ $key ]['required'] ?? false;
if ( $required || ! $hidden ) {
$field_keys_to_validate[] = $key;
}
}
return $field_keys_to_validate;
}
/**
* Get address fields for the passed country.
*
* @param string $country The country.
* @param string $type The address type. Default is 'billing_'. Possible values are 'billing_' and 'shipping_'.
*
* @return array
*/
public static function get_country_address_fields( string $country, string $type = 'billing_' ): array {
static $fields_for_country;
if ( null !== $fields_for_country ) {
return $fields_for_country;
}
$fields_for_country = WC()->countries->get_address_fields( $country, $type );
return $fields_for_country;
}
/**
* Format an address array into a single string for HTML.
*
* @param array $address_data Address data array.
* @return string Formatted address.
*/
public static function address_array_to_formatted_html_string( array $address_data ) {
$parts = array();
if ( ! empty( $address_data['company'] ) ) {
$parts[] = $address_data['company'];
}
if ( ! empty( $address_data['name'] ) ) {
$parts[] = $address_data['name'];
}
if ( ! empty( $address_data['address'] ) ) {
$parts[] = $address_data['address'];
}
$city_line = array_filter(
array(
$address_data['city'] ?? '',
$address_data['state'] ?? '',
$address_data['postcode'] ?? '',
)
);
if ( ! empty( $city_line ) ) {
$parts[] = implode( ', ', $city_line );
}
if ( ! empty( $address_data['country'] ) ) {
$parts[] = $address_data['country'];
}
if ( ! empty( $address_data['phone'] ) ) {
$parts[] = 'Phone: ' . $address_data['phone'];
}
return implode( '<br/>', $parts );
}
}