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"
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.



236
237
238
239
240
241
242
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
# File 'lib/worldwide/region.rb', line 236

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



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

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



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

def address1_regex
  @address1_regex
end

#alpha_threeObject (readonly)

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



50
51
52
# File 'lib/worldwide/region.rb', line 50

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.



59
60
61
# File 'lib/worldwide/region.rb', line 59

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.



55
56
57
# File 'lib/worldwide/region.rb', line 55

def building_number_required
  @building_number_required
end

#cldr_codeObject (readonly)

The CLDR code for this region.



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

def cldr_code
  @cldr_code
end

#code_alternatesObject

Alternate codes which may be used to designate this region



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

def code_alternates
  @code_alternates
end

#combined_address_formatObject

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



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

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.



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

def currency
  @currency
end

#example_addressObject

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



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

def example_address
  @example_address
end

#example_cityObject

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



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

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



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

def example_city_zip
  @example_city_zip
end

#flagObject

Unicode codepoints for this region’s flag emoji



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

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


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

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


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

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"


102
103
104
# File 'lib/worldwide/region.rb', line 102

def group
  @group
end

#group_nameObject

The continent that this region is part of.



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

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).



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

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



114
115
116
# File 'lib/worldwide/region.rb', line 114

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”).



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

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.



126
127
128
# File 'lib/worldwide/region.rb', line 126

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.



131
132
133
# File 'lib/worldwide/region.rb', line 131

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.



136
137
138
# File 'lib/worldwide/region.rb', line 136

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”.



140
141
142
# File 'lib/worldwide/region.rb', line 140

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.



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

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”.



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

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)



46
47
48
# File 'lib/worldwide/region.rb', line 46

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.



155
156
157
# File 'lib/worldwide/region.rb', line 155

def partial_zip_regex
  @partial_zip_regex
end

#phone_number_prefixObject

The telephone country dialing code for this region



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

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.



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

def priority
  @priority
end

#province_optionalObject

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



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

def province_optional
  @province_optional
end

#tagsObject

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



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

def tags
  @tags
end

#tax_inclusiveObject

Whether the region uses tax-inclusive pricing.



173
174
175
# File 'lib/worldwide/region.rb', line 173

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.



170
171
172
# File 'lib/worldwide/region.rb', line 170

def tax_name
  @tax_name
end

#tax_rateObject (readonly)

“generic” VAT tax rate on “most” goods



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

def tax_rate
  @tax_rate
end

#tax_typeObject

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



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

def tax_type
  @tax_type
end

#timezoneObject

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



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

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.



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

def timezones
  @timezones
end

#unit_systemObject

The measurement system in use in this region.



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

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



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

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”



194
195
196
# File 'lib/worldwide/region.rb', line 194

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.



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

def zip_autofill_enabled
  @zip_autofill_enabled
end

#zip_exampleObject

An example of a valid postal code for this region



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

def zip_example
  @zip_example
end

#zip_prefixesObject

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



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

def zip_prefixes
  @zip_prefixes
end

#zip_regexObject

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



216
217
218
# File 'lib/worldwide/region.rb', line 216

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)


469
470
471
# File 'lib/worldwide/region.rb', line 469

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

#zips_crossing_provincesObject

Hash of zips that are valid for more than one province



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

def zips_crossing_provinces
  @zips_crossing_provinces
end

#zonesObject (readonly)

Regions that are sub-regions of this region.



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

def zones
  @zones
end

Instance Method Details

#add_zone(region) ⇒ Object

Relationships



310
311
312
313
314
315
316
# File 'lib/worldwide/region.rb', line 310

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

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

#associated_continentObject



326
327
328
329
330
331
332
333
334
335
# File 'lib/worldwide/region.rb', line 326

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



320
321
322
323
324
# File 'lib/worldwide/region.rb', line 320

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.



344
345
346
# File 'lib/worldwide/region.rb', line 344

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.



339
340
341
# File 'lib/worldwide/region.rb', line 339

def autofill_zip
  zip_example if @zip_autofill_enabled
end

#city_required?Boolean

Does this region require cities to be specified?

Returns:

  • (Boolean)


349
350
351
# File 'lib/worldwide/region.rb', line 349

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

#continent?Boolean

Is this Region a continent?

Returns:

  • (Boolean)


354
355
356
# File 'lib/worldwide/region.rb', line 354

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)


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

def country?
  @country
end

#deprecated?Boolean

Returns:

  • (Boolean)


364
365
366
# File 'lib/worldwide/region.rb', line 364

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.



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

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.



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

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)


393
394
395
# File 'lib/worldwide/region.rb', line 393

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

#has_zip?Boolean

Does this region have postal codes?

Returns:

  • (Boolean)


388
389
390
# File 'lib/worldwide/region.rb', line 388

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)


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

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

#inspectObject



304
305
306
# File 'lib/worldwide/region.rb', line 304

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

#neighborhood_required?Boolean

is a neighborhood required for this region?

Returns:

  • (Boolean)


474
475
476
# File 'lib/worldwide/region.rb', line 474

def neighborhood_required?
  additional_field_required?("neighborhood")
end

#province?Boolean

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

Returns:

  • (Boolean)


398
399
400
# File 'lib/worldwide/region.rb', line 398

def province?
  @province
end

#province_optional?Boolean

are zones optional for this region?

Returns:

  • (Boolean)


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

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”.



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

def short_name
  @short_name || full_name
end

#street_name_required?Boolean

is a street name required for this region?

Returns:

  • (Boolean)


479
480
481
# File 'lib/worldwide/region.rb', line 479

def street_name_required?
  additional_field_required?("streetName")
end

#street_number_required?Boolean

is a street number required for this region?

Returns:

  • (Boolean)


484
485
486
# File 'lib/worldwide/region.rb', line 484

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)


489
490
491
492
493
494
495
# File 'lib/worldwide/region.rb', line 489

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



450
451
452
# File 'lib/worldwide/region.rb', line 450

def zip_autofill
  zip_example if zip_autofill_enabled
end

#zip_required?Boolean

is a postal code required for this region?

Returns:

  • (Boolean)


455
456
457
458
459
460
461
# File 'lib/worldwide/region.rb', line 455

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

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

returns a Region that is a child of this Region



409
410
411
412
413
414
415
416
417
418
419
420
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
# File 'lib/worldwide/region.rb', line 409

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