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,
  :flag,
  :format,
  :group,
  :group_name,
  :cldr_code,
  :iso_code,
  :languages,
  :neighbours,
  :numeric_three,
  :week_start_day,
  :unit_system,
  :zip_autofill_enabled,
  :zip_example,
  :zip_regex,
  :zip_requirement,
]

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.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/worldwide/region.rb', line 194

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

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

  @parents = [].to_set
  @zones = []
end

Instance Attribute Details

#alpha_threeObject (readonly)

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



43
44
45
# File 'lib/worldwide/region.rb', line 43

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.



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

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.



48
49
50
# File 'lib/worldwide/region.rb', line 48

def building_number_required
  @building_number_required
end

#cldr_codeObject (readonly)

The CLDR code for this region.



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

def cldr_code
  @cldr_code
end

#code_alternatesObject

Alternate codes which may be used to designate this region



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

def code_alternates
  @code_alternates
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.



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

def currency
  @currency
end

#example_cityObject

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



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

def example_city
  @example_city
end

#flagObject

Unicode codepoints for this region’s flag emoji



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

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


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

def format
  @format
end

#groupObject

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

Examples:

CountryDb.country(code: "CA").group == "North American Countries"


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

def group
  @group
end

#group_nameObject

The continent that this region is part of.



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

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



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

def hide_provinces_from_addresses
  @hide_provinces_from_addresses
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”).



94
95
96
# File 'lib/worldwide/region.rb', line 94

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.



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

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. This is the code that was traditionally returned by “country_db”.



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

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. This name is the name that was traditionally returned by “country_db”.



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

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



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

def name_alternates
  @name_alternates
end

#neighboursObject

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.



119
120
121
# File 'lib/worldwide/region.rb', line 119

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



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

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)



39
40
41
# File 'lib/worldwide/region.rb', line 39

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.



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

def partial_zip_regex
  @partial_zip_regex
end

#phone_number_prefixObject

The telephone country dialing code for this region



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

def phone_number_prefix
  @phone_number_prefix
end

#province_optionalObject

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



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

def province_optional
  @province_optional
end

#tagsObject

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



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

def tags
  @tags
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.



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

def tax_name
  @tax_name
end

#tax_rateObject (readonly)

“generic” VAT tax rate on “most” goods



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

def tax_rate
  @tax_rate
end

#timezoneObject

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



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

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.



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

def timezones
  @timezones
end

#unit_systemObject

The measurement system in use in this region.



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

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



167
168
169
# File 'lib/worldwide/region.rb', line 167

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”



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

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.



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

def zip_autofill_enabled
  @zip_autofill_enabled
end

#zip_exampleObject

An example of a valid postal code for this region



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

def zip_example
  @zip_example
end

#zip_prefixesObject

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



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

def zip_prefixes
  @zip_prefixes
end

#zip_regexObject

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



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

def zip_regex
  @zip_regex
end

#zip_requirementObject

Is a zip value required in this region? (Possible values: “optional”, “recommended”, “required”)



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

def zip_requirement
  @zip_requirement
end

#zips_crossing_provincesObject

Hash of zips that are valid for more than one province



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

def zips_crossing_provinces
  @zips_crossing_provinces
end

#zonesObject (readonly)

Regions that are sub-regions of this region.



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

def zones
  @zones
end

Instance Method Details

#add_zone(region) ⇒ Object

Relationships



260
261
262
263
264
265
# File 'lib/worldwide/region.rb', line 260

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

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

#associated_continentObject



275
276
277
278
279
280
281
282
283
284
# File 'lib/worldwide/region.rb', line 275

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



269
270
271
272
273
# File 'lib/worldwide/region.rb', line 269

def associated_country
  return self if country?

  parent_country
end

#autofill_zipObject

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



288
289
290
# File 'lib/worldwide/region.rb', line 288

def autofill_zip
  zip_example if @zip_autofill_enabled
end

#city_required?Boolean

Does this region require cities to be specified?

Returns:

  • (Boolean)


293
294
295
# File 'lib/worldwide/region.rb', line 293

def city_required?
  field(key: :city).autofill(locale: :en).nil?
end

#continent?Boolean

Is this Region a continent?

Returns:

  • (Boolean)


298
299
300
# File 'lib/worldwide/region.rb', line 298

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)


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

def country?
  @country
end

#deprecated?Boolean

Returns:

  • (Boolean)


308
309
310
# File 'lib/worldwide/region.rb', line 308

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.



314
315
316
317
318
# File 'lib/worldwide/region.rb', line 314

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.



321
322
323
324
325
326
327
328
# File 'lib/worldwide/region.rb', line 321

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_zip?Boolean

Does this region have postal codes?

Returns:

  • (Boolean)


331
332
333
# File 'lib/worldwide/region.rb', line 331

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

#inspectObject



254
255
256
# File 'lib/worldwide/region.rb', line 254

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

#province?Boolean

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

Returns:

  • (Boolean)


336
337
338
# File 'lib/worldwide/region.rb', line 336

def province?
  @province
end

#province_optional?Boolean

are zones optional for this region?

Returns:

  • (Boolean)


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

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



342
343
344
# File 'lib/worldwide/region.rb', line 342

def short_name
  @short_name || full_name
end

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

is the given postal code value valid for this region?

Returns:

  • (Boolean)


401
402
403
404
405
406
407
# File 'lib/worldwide/region.rb', line 401

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



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

def zip_autofill
  return zip_example if zip_autofill_enabled
end

#zip_required?Boolean

is a postal code required for this region?

Returns:

  • (Boolean)


392
393
394
395
396
397
398
# File 'lib/worldwide/region.rb', line 392

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



347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/worldwide/region.rb', line 347

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]}"

    zones.find do |region|
      [search_code, alt_search_code].any? do |candidate|
        candidate == subdivision_code(region.iso_code) ||
          candidate == region.alpha_three ||
          candidate == region.iso_code ||
          candidate == region.legacy_code ||
          candidate == region.numeric_three ||
          region&.code_alternates&.any?(candidate)
      end
    end
  elsif Worldwide::Util.present?(name)
    search_name = name.upcase

    zones.find do |region|
      search_name == region.legacy_name.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))
  end || Worldwide.unknown_region
end