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.



184
185
186
187
188
189
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
# File 'lib/worldwide/region.rb', line 184

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
  @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

  @parent = nil
  @zones = []
end

Instance Attribute Details

#alpha_threeObject (readonly)

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



41
42
43
# File 'lib/worldwide/region.rb', line 41

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



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

def building_number_required
  @building_number_required
end

#cldr_codeObject (readonly)

The CLDR code for this region.



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

def cldr_code
  @cldr_code
end

#code_alternatesObject

Alternate codes which may be used to designate this region



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

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.



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

def currency
  @currency
end

#example_cityObject

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



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

def example_city
  @example_city
end

#flagObject

Unicode codepoints for this region’s flag emoji



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

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


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

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"


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

def group
  @group
end

#group_nameObject

The continent that this region is part of.



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

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



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

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



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

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.



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

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



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

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



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

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.



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

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



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

def numeric_three
  @numeric_three
end

#parentObject

Returns the value of attribute parent.



37
38
39
# File 'lib/worldwide/region.rb', line 37

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



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

def partial_zip_regex
  @partial_zip_regex
end

#phone_number_prefixObject

The telephone country dialing code for this region



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

def phone_number_prefix
  @phone_number_prefix
end

#province_optionalObject

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



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

def province_optional
  @province_optional
end

#tagsObject

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



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

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.



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

def tax_name
  @tax_name
end

#tax_rateObject (readonly)

“generic” VAT tax rate on “most” goods



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

def tax_rate
  @tax_rate
end

#timezoneObject

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



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

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.



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

def timezones
  @timezones
end

#unit_systemObject

The measurement system in use in this region.



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

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



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

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”



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

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.



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

def zip_autofill_enabled
  @zip_autofill_enabled
end

#zip_exampleObject

An example of a valid postal code for this region



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

def zip_example
  @zip_example
end

#zip_prefixesObject

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



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

def zip_prefixes
  @zip_prefixes
end

#zip_regexObject

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



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

def zip_regex
  @zip_regex
end

#zip_requirementObject

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



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

def zip_requirement
  @zip_requirement
end

#zips_crossing_provincesObject

Hash of zips that are valid for more than one province



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

def zips_crossing_provinces
  @zips_crossing_provinces
end

#zonesObject (readonly)

Regions that are sub-regions of this region.



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

def zones
  @zones
end

Instance Method Details

#add_zone(region) ⇒ Object

Relationships



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

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

  region.parent = self
  @zones.append(region)
end

#autofill_zipObject

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



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

def autofill_zip
  zip_example if @zip_autofill_enabled
end

#city_required?Boolean

Does this region require cities to be specified?

Returns:

  • (Boolean)


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

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

#continent?Boolean

Is this Region a continent?

Returns:

  • (Boolean)


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

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)


276
277
278
# File 'lib/worldwide/region.rb', line 276

def country?
  @country
end

#deprecated?Boolean

Returns:

  • (Boolean)


280
281
282
# File 'lib/worldwide/region.rb', line 280

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.



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

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.



293
294
295
296
297
298
299
300
# File 'lib/worldwide/region.rb', line 293

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)


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

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

#inspectObject



243
244
245
# File 'lib/worldwide/region.rb', line 243

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)


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

def province?
  @province
end

#province_optional?Boolean

are zones optional for this region?

Returns:

  • (Boolean)


381
382
383
# File 'lib/worldwide/region.rb', line 381

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



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

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)


372
373
374
375
376
377
378
# File 'lib/worldwide/region.rb', line 372

def valid_zip?(zip, partial_match: false)
  normalized = Zip.normalize(
    country_code: province? && parent.iso_code ? parent.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



358
359
360
# File 'lib/worldwide/region.rb', line 358

def zip_autofill
  return zip_example if zip_autofill_enabled
end

#zip_required?Boolean

is a postal code required for this region?

Returns:

  • (Boolean)


363
364
365
366
367
368
369
# File 'lib/worldwide/region.rb', line 363

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



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/worldwide/region.rb', line 319

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 == 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