Class: Worldwide::Region

Inherits:
Object
  • Object
show all
Defined in:
lib/worldwide/region.rb

Constant Summary collapse

REQUIRED =

When faced with the question, “Is a postal code required in this country?”, we treat the answer as a tri-state configuration item. “Recommended” means that we recommend that it be provided, but you may still leave the zip field blank.

"required"
"recommended"
OPTIONAL =
"optional"
FORMAT_TYPES =

Accpetable format types of the postal code.

{
  ALPHANUMERIC: "ALPHANUMERIC",
  NUMERIC: "NUMERIC",
  NUMERIC_AND_PUNCTUATION: "NUMERIC_AND_PUNCTUATION",
}
INSPECTION_FIELDS =

The default ‘.inspect` isn’t a good fit for Region, because it can end up dumping a lot of info as it walks the hierarchy of descendants. So, instead, we provide our own ‘.inspect` that only shows a restricted subset of the object’s fields.

[
  :alpha_three,
  :building_number_required,
  :currency,
  :example_city,
  :example_city_zip,
  :example_address,
  :flag,
  :format,
  :format_extended,
  :group,
  :group_name,
  :cldr_code,
  :iso_code,
  :languages,
  :neighbours,
  :numeric_three,
  :priority,
  :week_start_day,
  :unit_system,
  :zip_autofill_enabled,
  :zip_example,
  :zip_regex,
  :zip_requirement,
  :additional_address_fields,
  :combined_address_format,
  :address1_regex,
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(alpha_three: nil, continent: false, country: false, deprecated: false, cldr_code: nil, iso_code: nil, legacy_code: nil, legacy_name: nil, numeric_three: nil, province: false, short_name: nil, tax_name: nil, tax_rate: 0.0, use_zone_code_as_short_name: false) ⇒ Region

Returns a new instance of Region.



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/worldwide/region.rb', line 243

def initialize(
  alpha_three: nil,
  continent: false,
  country: false,
  deprecated: false,
  cldr_code: nil,
  iso_code: nil,
  legacy_code: nil,
  legacy_name: nil,
  numeric_three: nil,
  province: false,
  short_name: nil,
  tax_name: nil,
  tax_rate: 0.0,
  use_zone_code_as_short_name: false
)
  if iso_code.nil? && numeric_three.nil?
    raise ArgumentError, "At least one of iso_code: and numeric_three: must be provided"
  end

  @alpha_three = alpha_three&.to_s&.upcase
  @continent = continent
  @country = country
  @deprecated = deprecated
  @cldr_code = cldr_code
  @iso_code = iso_code&.to_s&.upcase
  @legacy_code = legacy_code
  @legacy_name = legacy_name
  @numeric_three = numeric_three&.to_s
  @province = province
  @short_name = short_name
  @tax_name = tax_name
  @tax_rate = tax_rate
  @use_zone_code_as_short_name = use_zone_code_as_short_name

  @additional_address_fields = []
  @combined_address_format = {}
  @address1_regex = []
  @building_number_required = false
  @building_number_may_be_in_address2 = false
  @currency = nil
  @flag = nil
  @format = {}
  @format_extended = {}
  @name_alternates = []
  @group = nil
  @group_name = nil
  @languages = []
  @neighbours = []
  @partial_zip_regex = nil
  @phone_number_prefix = nil
  @tags = []
  @tax_inclusive = false
  @timezone = nil
  @timezones = {}
  @unit_system = nil
  @week_start_day = nil
  @zip_autofill_enabled = false
  @zip_example = nil
  @zip_prefixes = []
  @zip_regex = nil
  @example_address = nil

  @parents = [].to_set
  @zones = []
  @zones_by_code = {}
end

Instance Attribute Details

#additional_address_fieldsObject

An array of the additional address fields that are defined for this region



235
236
237
# File 'lib/worldwide/region.rb', line 235

def additional_address_fields
  @additional_address_fields
end

#address1_regexObject

An array of regex patterns of the address1 field, capturing the supported additional address fields



241
242
243
# File 'lib/worldwide/region.rb', line 241

def address1_regex
  @address1_regex
end

#alpha_threeObject (readonly)

ISO-3166 three-letter code for this region, if there is one. Otherwise, nil.



57
58
59
# File 'lib/worldwide/region.rb', line 57

def alpha_three
  @alpha_three
end

#building_number_may_be_in_address2Object

In some countries, an address may have the building number in address2. If we are allowed to have a building number in address2, then this will be true.



66
67
68
# File 'lib/worldwide/region.rb', line 66

def building_number_may_be_in_address2
  @building_number_may_be_in_address2
end

#building_number_requiredObject

In some countries, every address must have a building number. In others (e.g., GB), some addresses rely on just a building name, not a number. If we require a building number in an address, then this will be true.



62
63
64
# File 'lib/worldwide/region.rb', line 62

def building_number_required
  @building_number_required
end

#cldr_codeObject (readonly)

The CLDR code for this region.



124
125
126
# File 'lib/worldwide/region.rb', line 124

def cldr_code
  @cldr_code
end

#code_alternatesObject

Alternate codes which may be used to designate this region



69
70
71
# File 'lib/worldwide/region.rb', line 69

def code_alternates
  @code_alternates
end

#combined_address_formatObject

A hash of the rules for concatening the additional address fields into the standard fields



238
239
240
# File 'lib/worldwide/region.rb', line 238

def combined_address_format
  @combined_address_format
end

#currencyObject

The suggested currency for use in this region. Note that this may not always be the official currency. E.g., we return USD for VE, not VED.



74
75
76
# File 'lib/worldwide/region.rb', line 74

def currency
  @currency
end

#example_addressObject

A full address in the given region that can be used as an example



87
88
89
# File 'lib/worldwide/region.rb', line 87

def example_address
  @example_address
end

#example_cityObject

A major city in the given region that can be used as an example



77
78
79
# File 'lib/worldwide/region.rb', line 77

def example_city
  @example_city
end

#example_city_zipObject

A zip code in the given region that can be used as an example; corresponds to example_city



80
81
82
# File 'lib/worldwide/region.rb', line 80

def example_city_zip
  @example_city_zip
end

#flagObject

Unicode codepoints for this region’s flag emoji



90
91
92
# File 'lib/worldwide/region.rb', line 90

def flag
  @flag
end

#formatObject

Hash of strings denoting how to format an address in this region. The format is described in shopify.engineering/handling-addresses-from-all-around-the-world

- address1: a street address (address line 1, with a buliding nmuber and street name)
- address1_with_unit: address line 1 including a subpremise (unit, apartment, etc.)
- edit: the fields to present on an address input form
- show: how to arrange the fields when formatting an address for display


98
99
100
# File 'lib/worldwide/region.rb', line 98

def format
  @format
end

#format_extendedObject

Hash of strings denoting how to format an address in this region, including substitute and/or additional fields. The format is described in shopify.engineering/handling-addresses-from-all-around-the-world

- edit: the fields to present on an address input form
- show: how to arrange the fields when formatting an address for display


104
105
106
# File 'lib/worldwide/region.rb', line 104

def format_extended
  @format_extended
end

#groupObject

The string that results from appending “ Countries” to the adjectival form of the #group_name

Examples:

Worldwide.region(code: "CA").group == "North American Countries"


109
110
111
# File 'lib/worldwide/region.rb', line 109

def group
  @group
end

#group_nameObject

The continent that this region is part of.



112
113
114
# File 'lib/worldwide/region.rb', line 112

def group_name
  @group_name
end

#hide_provinces_from_addressesObject

If this flag is set, then we support provinces “under the hood” for this country, but we do not show them as part of a formatted address. If the province is missing, we will auto-infer it based on the zip (note that this auto-inference may be wrong for some addresses near a border).



117
118
119
# File 'lib/worldwide/region.rb', line 117

def hide_provinces_from_addresses
  @hide_provinces_from_addresses
end

#ignore_provincesObject

Returns true if provinces should be ignored Used when adding provinces to a country that has none as an intermediate step



121
122
123
# File 'lib/worldwide/region.rb', line 121

def ignore_provinces
  @ignore_provinces
end

#iso_codeObject (readonly)

The ISO-3166-2 code for this region (e.g. “CA”, “CA-ON”) or, if there is no alpha-2 code defined for this region, a numeric code (e.g. “001”).



128
129
130
# File 'lib/worldwide/region.rb', line 128

def iso_code
  @iso_code
end

#languagesObject

Languages that are commonly used in this region. Note that this may not be the same as the languages that are officially recognized there. We present them in alphabetical order by language code.



133
134
135
# File 'lib/worldwide/region.rb', line 133

def languages
  @languages
end

#legacy_codeObject (readonly)

The code used by the legacy Shopify ecosystem for this region. E.g., for MX-CMX it will return “DF”. This code should never be shown in the user interface.



138
139
140
# File 'lib/worldwide/region.rb', line 138

def legacy_code
  @legacy_code
end

#legacy_nameObject (readonly)

The name used by the legacy Shopify ecosystem for this region. E.g., “Sao Tome And Principe” for “ST”. This name should never be shown in the user interface.



143
144
145
# File 'lib/worldwide/region.rb', line 143

def legacy_name
  @legacy_name
end

#name_alternatesObject

Other names that may be used to refer to this region. E.g., “Czech Republic” is also known as “Czechia”.



147
148
149
# File 'lib/worldwide/region.rb', line 147

def name_alternates
  @name_alternates
end

#neighboursObject Also known as: neighbors

iso_code values of regions (subdivisions) within the same country that border this region. E.g., for CA-ON, the neighbouring zones are CA-MB, CA-NU and CA-QC.



151
152
153
# File 'lib/worldwide/region.rb', line 151

def neighbours
  @neighbours
end

#numeric_threeObject (readonly)

The ISO-3166-1 three-digit code for this region (returned as a string to preserve leading zeroes), e.g., “003”.



157
158
159
# File 'lib/worldwide/region.rb', line 157

def numeric_three
  @numeric_three
end

#parentsObject

A region may have more than one parent. For example, Puerto Rico (PR/US-PR) is associated with both the US and the Caribbean (029)



53
54
55
# File 'lib/worldwide/region.rb', line 53

def parents
  @parents
end

#partial_zip_regexObject

Some countries have a multi-part postal code, and we may in some cases encounter only the first part. E.g., the GB code ‘SW1A 1AA` has a first part (outward code) of `SW1A`. When validating such a partial postal code, it must match this regular expression.



162
163
164
# File 'lib/worldwide/region.rb', line 162

def partial_zip_regex
  @partial_zip_regex
end

#phone_number_prefixObject

The telephone country dialing code for this region



165
166
167
# File 'lib/worldwide/region.rb', line 165

def phone_number_prefix
  @phone_number_prefix
end

#priorityObject

A value that can be used to order zones Some countries, Japan, for example, customarily order zones non-alphabetically.



84
85
86
# File 'lib/worldwide/region.rb', line 84

def priority
  @priority
end

#province_optionalObject

If true, then the province is optional for addresses in this region.



232
233
234
# File 'lib/worldwide/region.rb', line 232

def province_optional
  @province_optional
end

#tagsObject

tags that help us group the region, e.g. “EU-member”



189
190
191
# File 'lib/worldwide/region.rb', line 189

def tags
  @tags
end

#tax_inclusiveObject

Whether the region uses tax-inclusive pricing.



180
181
182
# File 'lib/worldwide/region.rb', line 180

def tax_inclusive
  @tax_inclusive
end

#tax_nameObject (readonly)

Value Added Tax (Sales Tax) name Note that this should really be translated; showing this untranslated name to users is a bad idea.



177
178
179
# File 'lib/worldwide/region.rb', line 177

def tax_name
  @tax_name
end

#tax_rateObject (readonly)

“generic” VAT tax rate on “most” goods



183
184
185
# File 'lib/worldwide/region.rb', line 183

def tax_rate
  @tax_rate
end

#tax_typeObject

The type of tax for this region, e.g. “harmonized”



186
187
188
# File 'lib/worldwide/region.rb', line 186

def tax_type
  @tax_type
end

#timezoneObject

If the region is within a single timezone, its Olson name will be given here.



192
193
194
# File 'lib/worldwide/region.rb', line 192

def timezone
  @timezone
end

#timezonesObject

If the region spans multiple timezones (and it has postal codes), then this attribute will contain a hash table mapping from timezone name to a list of postal code prefixes. We can use this information to determine the timezone for a given postal code.



197
198
199
# File 'lib/worldwide/region.rb', line 197

def timezones
  @timezones
end

#unit_systemObject

The measurement system in use in this region.



204
205
206
# File 'lib/worldwide/region.rb', line 204

def unit_system
  @unit_system
end

#use_zone_code_as_short_nameObject

true iff zone.iso_code should be returned as the .short_name for zones of this region



207
208
209
# File 'lib/worldwide/region.rb', line 207

def use_zone_code_as_short_name
  @use_zone_code_as_short_name
end

#week_start_dayObject

Day of the week (English language string) on which the week is considered to start in this region. E.g., “sunday”



201
202
203
# File 'lib/worldwide/region.rb', line 201

def week_start_day
  @week_start_day
end

#zip_autofill_enabledObject

Some regions have only a single postal code value. In such cases, we can autofill the zip field with the value from zip_example.



211
212
213
# File 'lib/worldwide/region.rb', line 211

def zip_autofill_enabled
  @zip_autofill_enabled
end

#zip_exampleObject

An example of a valid postal code for this region



214
215
216
# File 'lib/worldwide/region.rb', line 214

def zip_example
  @zip_example
end

#zip_prefixesObject

A list of character sequences with which a postal code in this region may start.



220
221
222
# File 'lib/worldwide/region.rb', line 220

def zip_prefixes
  @zip_prefixes
end

#zip_regexObject

A regular expression which postal codes in this region must match.



223
224
225
# File 'lib/worldwide/region.rb', line 223

def zip_regex
  @zip_regex
end

#zip_requirementString

Returns whether (and how firmly) we require a value in the zip field Possible returns:

- "required" means a value must be supplied
- "recommended" means a value is optional, but we recommend providing one
- "optional" means a value is optional, and we say it is optional

Returns:

  • (String)


499
500
501
# File 'lib/worldwide/region.rb', line 499

def zip_requirement
  @zip_requirement || (zip_required? ? REQUIRED : OPTIONAL)
end

#zips_crossing_provincesObject

Hash of zips that are valid for more than one province



226
227
228
# File 'lib/worldwide/region.rb', line 226

def zips_crossing_provinces
  @zips_crossing_provinces
end

#zonesObject (readonly)

Regions that are sub-regions of this region.



229
230
231
# File 'lib/worldwide/region.rb', line 229

def zones
  @zones
end

Instance Method Details

#add_zone(region) ⇒ Object

Relationships



317
318
319
320
321
322
323
# File 'lib/worldwide/region.rb', line 317

def add_zone(region)
  return if @zones.include?(region)

  region.parents << self
  @zones.append(region)
  add_zone_to_hash(region)
end

#associated_continentObject



333
334
335
336
337
338
339
340
341
342
# File 'lib/worldwide/region.rb', line 333

def associated_continent
  return self if continent?

  parents.each do |parent|
    candidate = parent.associated_continent
    return candidate unless candidate.nil?
  end

  nil
end

#associated_countryObject

Attributes



327
328
329
330
331
# File 'lib/worldwide/region.rb', line 327

def associated_country
  return self if country?

  parent_country
end

#autofill_city(locale: I18n.locale) ⇒ Object

The value with which to autofill the city, if this region has a default city; otherwise, nil.



351
352
353
# File 'lib/worldwide/region.rb', line 351

def autofill_city(locale: I18n.locale)
  field(key: :city).autofill(locale: locale)
end

#autofill_zipObject

The value with which to autofill the zip, if this region has zip autofill active; otherwise, nil.



346
347
348
# File 'lib/worldwide/region.rb', line 346

def autofill_zip
  zip_example if @zip_autofill_enabled
end

#city_required?Boolean

Does this region require cities to be specified?

Returns:

  • (Boolean)


356
357
358
# File 'lib/worldwide/region.rb', line 356

def city_required?
  autofill_city(locale: :en).nil?
end

#continent?Boolean

Is this Region a continent?

Returns:

  • (Boolean)


361
362
363
# File 'lib/worldwide/region.rb', line 361

def continent?
  @continent
end

#country?Boolean

Is this Region considered a “country” (top-level political entity “country or region”) in the view of the legacy Shopify ecosystem?

Returns:

  • (Boolean)


367
368
369
# File 'lib/worldwide/region.rb', line 367

def country?
  @country
end

#deprecated?Boolean

Returns:

  • (Boolean)


371
372
373
# File 'lib/worldwide/region.rb', line 371

def deprecated?
  @deprecated
end

#field(key:) ⇒ Object

An Worldwide::Field that can be used to ask about the field, including labels, error messages, and an autofill value if there is one.



377
378
379
380
381
# File 'lib/worldwide/region.rb', line 377

def field(key:)
  return nil unless country?

  Worldwide::Fields.field(country_code: iso_code, field_key: key)
end

#full_name(locale: I18n.locale) ⇒ Object

A user-facing name in the currently-active locale’s language.



384
385
386
387
388
389
390
391
392
# File 'lib/worldwide/region.rb', line 384

def full_name(locale: I18n.locale)
  lookup_code = cldr_code

  if /^[0-9]+$/.match?(lookup_code) || lookup_code.length < 3
    Worldwide::Cldr.t("territories.#{lookup_code}", locale: locale, default: legacy_name)
  else
    Worldwide::Cldr.t("subdivisions.#{lookup_code}", locale: locale, default: legacy_name)
  end
end

#has_cities?Boolean

Does this region have cities?

Returns:

  • (Boolean)


400
401
402
# File 'lib/worldwide/region.rb', line 400

def has_cities?
  !!format["show"]&.include?("{city}")
end

#has_provinces?Boolean

Does this region have provinces in their addresses?

Returns:

  • (Boolean)


405
406
407
# File 'lib/worldwide/region.rb', line 405

def has_provinces?
  !!format["show"]&.include?("{province}")
end

#has_zip?Boolean

Does this region have postal codes?

Returns:

  • (Boolean)


395
396
397
# File 'lib/worldwide/region.rb', line 395

def has_zip?
  !!format["show"]&.include?("{zip}")
end

#has_zip_prefixes?Boolean

Returns true if this country has zones defined, and has postal code prefix data for the zones

Returns:

  • (Boolean)


533
534
535
536
537
# File 'lib/worldwide/region.rb', line 533

def has_zip_prefixes?
  @zones&.any? do |zone|
    Util.present?(zone.zip_prefixes)
  end
end

#inspectObject



311
312
313
# File 'lib/worldwide/region.rb', line 311

def inspect
  "#<#{self.class.name}:#{object_id} #{inspected_fields}>"
end

#neighborhood_required?Boolean

is a neighborhood required for this region?

Returns:

  • (Boolean)


504
505
506
# File 'lib/worldwide/region.rb', line 504

def neighborhood_required?
  additional_field_required?("neighborhood")
end

#province?Boolean

Is this Region considered a “province” (political subdivision of a “country”)?

Returns:

  • (Boolean)


410
411
412
# File 'lib/worldwide/region.rb', line 410

def province?
  @province
end

#province_optional?Boolean

are zones optional for this region?

Returns:

  • (Boolean)


528
529
530
# File 'lib/worldwide/region.rb', line 528

def province_optional?
  province_optional || !@zones&.any?(&:province?)
end

#short_nameObject

A short-form name for this region, if there is a conventional short form. E.g., returns “ON” for “CA-ON”, but “Tokyo” for “JP-13”.



416
417
418
# File 'lib/worldwide/region.rb', line 416

def short_name
  @short_name || full_name
end

#street_name_required?Boolean

is a street name required for this region?

Returns:

  • (Boolean)


509
510
511
# File 'lib/worldwide/region.rb', line 509

def street_name_required?
  additional_field_required?("streetName")
end

#street_number_required?Boolean

is a street number required for this region?

Returns:

  • (Boolean)


514
515
516
# File 'lib/worldwide/region.rb', line 514

def street_number_required?
  additional_field_required?("streetNumber")
end

#valid_zip?(zip, partial_match: false) ⇒ Boolean

is the given postal code value valid for this region?

Returns:

  • (Boolean)


519
520
521
522
523
524
525
# File 'lib/worldwide/region.rb', line 519

def valid_zip?(zip, partial_match: false)
  normalized = Zip.normalize(
    country_code: province? && associated_country.iso_code ? associated_country.iso_code : iso_code,
    zip: zip,
  )
  valid_normalized_zip?(normalized, partial_match: partial_match)
end

#zip_autofillObject

If the Region has an autofill zip, return the value that will be autofilled Otherwise, return nil



462
463
464
# File 'lib/worldwide/region.rb', line 462

def zip_autofill
  zip_example if zip_autofill_enabled
end

#zip_required?Boolean

is a postal code required for this region?

Returns:

  • (Boolean)


467
468
469
470
471
472
473
# File 'lib/worldwide/region.rb', line 467

def zip_required?
  if @zip_requirement.nil?
    !zip_regex.nil?
  else
    REQUIRED == @zip_requirement
  end
end

#zip_typeString?

Returns the format type of the postal code. Analyzes the postal code example and regex pattern to determine if it contains only numbers, alphanumeric characters, or numbers with punctuation/spaces. Mainly used to determine the type of keyboard to show on mobile views.

Returns:

  • (String, nil)

    One of “ALPHANUMERIC”, “NUMERIC”, “NUMERIC_AND_PUNCTUATION”, or nil if no zip_example



481
482
483
484
485
486
487
488
489
490
491
# File 'lib/worldwide/region.rb', line 481

def zip_type
  return nil if zip_example.nil?

  if zip_example.match?(/[A-Za-z]/)
    FORMAT_TYPES[:ALPHANUMERIC]
  elsif zip_example.match?(/[[:punct:]\s]/) || zip_regex&.match?(/}-|\\-|-[?+*]|\(-/)
    FORMAT_TYPES[:NUMERIC_AND_PUNCTUATION]
  else
    FORMAT_TYPES[:NUMERIC]
  end
end

#zone(code: nil, name: nil, zip: nil) ⇒ Object

returns a Region that is a child of this Region



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/worldwide/region.rb', line 421

def zone(code: nil, name: nil, zip: nil)
  count = 0
  count += 1 unless code.nil?
  count += 1 unless name.nil?
  count += 1 unless zip.nil?

  unless count == 1
    # More than one of code, name, or zip was given
    raise ArgumentError, "Must specify exactly one of code:, name: or zip:."
  end

  if Worldwide::Util.present?(code)
    search_code = code.to_s.upcase
    alt_search_code = "#{search_code[0..1]}-#{search_code[2..-1]}"
    zone = nil

    [search_code, alt_search_code].each do |candidate|
      zone = @zones_by_code[candidate]
      break if zone
    end

    return zone unless zone.nil?
  elsif Worldwide::Util.present?(name)
    search_name = name.upcase

    zones.find do |region|
      search_name == region.legacy_name&.upcase ||
        region.name_alternates&.any? { |a| search_name == a.upcase } ||
        search_name == region.full_name&.upcase ||
        search_name == I18n.with_locale(:en) { region.full_name&.upcase }
    end
  else # Worldwide::Util.present?(zip)
    zone_by_normalized_zip(
      Zip.normalize(country_code: iso_code, zip: zip),
      allow_partial_zip: hide_provinces_from_addresses,
    )
  end || Worldwide.unknown_region
end