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.



190
191
192
193
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
# File 'lib/worldwide/region.rb', line 190

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

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



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

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



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

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.



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

def partial_zip_regex
  @partial_zip_regex
end

#phone_number_prefixObject

The telephone country dialing code for this region



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

def phone_number_prefix
  @phone_number_prefix
end

#province_optionalObject

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



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

def province_optional
  @province_optional
end

#tagsObject

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



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

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.



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

def tax_name
  @tax_name
end

#tax_rateObject (readonly)

“generic” VAT tax rate on “most” goods



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

def tax_rate
  @tax_rate
end

#timezoneObject

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



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

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.



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

def timezones
  @timezones
end

#unit_systemObject

The measurement system in use in this region.



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

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



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

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”



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

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.



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

def zip_autofill_enabled
  @zip_autofill_enabled
end

#zip_exampleObject

An example of a valid postal code for this region



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

def zip_example
  @zip_example
end

#zip_prefixesObject

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



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

def zip_prefixes
  @zip_prefixes
end

#zip_regexObject

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



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

def zip_regex
  @zip_regex
end

#zip_requirementObject

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



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

def zip_requirement
  @zip_requirement
end

#zips_crossing_provincesObject

Hash of zips that are valid for more than one province



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

def zips_crossing_provinces
  @zips_crossing_provinces
end

#zonesObject (readonly)

Regions that are sub-regions of this region.



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

def zones
  @zones
end

Instance Method Details

#add_zone(region) ⇒ Object

Relationships



256
257
258
259
260
261
# File 'lib/worldwide/region.rb', line 256

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

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

#associated_countryObject

Attributes



265
266
267
268
269
# File 'lib/worldwide/region.rb', line 265

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.



273
274
275
# File 'lib/worldwide/region.rb', line 273

def autofill_zip
  zip_example if @zip_autofill_enabled
end

#city_required?Boolean

Does this region require cities to be specified?

Returns:

  • (Boolean)


278
279
280
# File 'lib/worldwide/region.rb', line 278

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

#continent?Boolean

Is this Region a continent?

Returns:

  • (Boolean)


283
284
285
# File 'lib/worldwide/region.rb', line 283

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)


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

def country?
  @country
end

#deprecated?Boolean

Returns:

  • (Boolean)


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

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.



299
300
301
302
303
# File 'lib/worldwide/region.rb', line 299

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.



306
307
308
309
310
311
312
313
# File 'lib/worldwide/region.rb', line 306

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)


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

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

#inspectObject



250
251
252
# File 'lib/worldwide/region.rb', line 250

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)


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

def province?
  @province
end

#province_optional?Boolean

are zones optional for this region?

Returns:

  • (Boolean)


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

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



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

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)


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

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



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

def zip_autofill
  return zip_example if zip_autofill_enabled
end

#zip_required?Boolean

is a postal code required for this region?

Returns:

  • (Boolean)


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

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



332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/worldwide/region.rb', line 332

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