addressing 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ {
2
+ "country_code": "GT",
3
+ "subdivisions": {
4
+ "16": {
5
+ "name": "Alta Verapaz"
6
+ },
7
+ "15": {
8
+ "name": "Baja Verapaz"
9
+ },
10
+ "04": {
11
+ "name": "Chimaltenango"
12
+ },
13
+ "20": {
14
+ "name": "Chiquimula"
15
+ },
16
+ "02": {
17
+ "name": "El Progreso"
18
+ },
19
+ "05": {
20
+ "name": "Escuintla"
21
+ },
22
+ "01": {
23
+ "name": "Guatemala"
24
+ },
25
+ "13": {
26
+ "name": "Huehuetenango"
27
+ },
28
+ "18": {
29
+ "name": "Izabal"
30
+ },
31
+ "21": {
32
+ "name": "Jalapa"
33
+ },
34
+ "22": {
35
+ "name": "Jutiapa"
36
+ },
37
+ "17": {
38
+ "name": "Petén"
39
+ },
40
+ "09": {
41
+ "name": "Quetzaltenango"
42
+ },
43
+ "14": {
44
+ "name": "Quiché"
45
+ },
46
+ "11": {
47
+ "name": "Retalhuleu"
48
+ },
49
+ "03": {
50
+ "name": "Sacatepéquez"
51
+ },
52
+ "12": {
53
+ "name": "San Marcos"
54
+ },
55
+ "06": {
56
+ "name": "Santa Rosa"
57
+ },
58
+ "07": {
59
+ "name": "Sololá"
60
+ },
61
+ "10": {
62
+ "name": "Suchitepéquez"
63
+ },
64
+ "08": {
65
+ "name": "Totonicapán"
66
+ },
67
+ "19": {
68
+ "name": "Zacapa"
69
+ }
70
+ }
71
+ }
@@ -149,6 +149,10 @@
149
149
  "code": "FR",
150
150
  "name": "Frosinone"
151
151
  },
152
+ "OT": {
153
+ "code": "OT",
154
+ "name": "Gallura Nord-Est Sardegna"
155
+ },
152
156
  "GE": {
153
157
  "code": "GE",
154
158
  "name": "Genova"
@@ -217,6 +221,10 @@
217
221
  "code": "MT",
218
222
  "name": "Matera"
219
223
  },
224
+ "VS": {
225
+ "code": "VS",
226
+ "name": "Medio Campidano"
227
+ },
220
228
  "ME": {
221
229
  "code": "ME",
222
230
  "name": "Messina"
@@ -245,6 +253,10 @@
245
253
  "code": "NU",
246
254
  "name": "Nuoro"
247
255
  },
256
+ "OG": {
257
+ "code": "OG",
258
+ "name": "Ogliastra"
259
+ },
248
260
  "OR": {
249
261
  "code": "OR",
250
262
  "name": "Oristano"
@@ -359,7 +371,7 @@
359
371
  },
360
372
  "SU": {
361
373
  "code": "SU",
362
- "name": "Sud Sardegna"
374
+ "name": "Sulcis Iglesiente"
363
375
  },
364
376
  "TA": {
365
377
  "code": "TA",
@@ -83,7 +83,7 @@
83
83
  "name": "Niger"
84
84
  },
85
85
  "OG": {
86
- "name": "Ogun State"
86
+ "name": "Ogun"
87
87
  },
88
88
  "ON": {
89
89
  "name": "Ondo"
@@ -240,10 +240,6 @@
240
240
  "name": "Tyva Republits",
241
241
  "local_name": "Республика Тыва"
242
242
  },
243
- "UD": {
244
- "name": "Udmurtskaja Respublika",
245
- "local_name": "Республика Удмуртия"
246
- },
247
243
  "KK": {
248
244
  "name": "Khakasiya Republits",
249
245
  "local_name": "Республика Хакасия"
@@ -308,6 +304,10 @@
308
304
  "name": "Tyumenskaya oblast'",
309
305
  "local_name": "Тюменская область"
310
306
  },
307
+ "UD": {
308
+ "name": "Udmurtskaja Respublika",
309
+ "local_name": "Удмуртская Республика"
310
+ },
311
311
  "ULY": {
312
312
  "name": "Ulyanovskaya oblast'",
313
313
  "local_name": "Ульяновская область"
@@ -26,10 +26,6 @@
26
26
  "name": "Khon Kaen",
27
27
  "local_name": "ขอนแก่น"
28
28
  },
29
- "38": {
30
- "name": "Bueng Kan",
31
- "local_name": "จังหวัด บึงกาฬ"
32
- },
33
29
  "22": {
34
30
  "name": "Chanthaburi",
35
31
  "local_name": "จันทบุรี"
@@ -51,7 +47,7 @@
51
47
  "local_name": "ชัยภูมิ"
52
48
  },
53
49
  "86": {
54
- "name": "Chumpon",
50
+ "name": "Chumphon",
55
51
  "local_name": "ชุมพร"
56
52
  },
57
53
  "57": {
@@ -110,6 +106,10 @@
110
106
  "name": "Nan",
111
107
  "local_name": "น่าน"
112
108
  },
109
+ "38": {
110
+ "name": "Bueng Kan",
111
+ "local_name": "บึงกาฬ"
112
+ },
113
113
  "31": {
114
114
  "name": "Buri Ram",
115
115
  "local_name": "บุรีรัมย์"
@@ -143,7 +143,7 @@
143
143
  "local_name": "พังงา"
144
144
  },
145
145
  "93": {
146
- "name": "Phattalung",
146
+ "name": "Phatthalung",
147
147
  "local_name": "พัทลุง"
148
148
  },
149
149
  "66": {
@@ -267,7 +267,7 @@
267
267
  "local_name": "สุโขทัย"
268
268
  },
269
269
  "72": {
270
- "name": "Suphanburi",
270
+ "name": "Suphan Buri",
271
271
  "local_name": "สุพรรณบุรี"
272
272
  },
273
273
  "84": {
@@ -8,7 +8,7 @@
8
8
  "name": "Adıyaman"
9
9
  },
10
10
  "03": {
11
- "name": "Afyon"
11
+ "name": "Afyonkarahisar"
12
12
  },
13
13
  "04": {
14
14
  "name": "Ağrı"
@@ -32,7 +32,7 @@
32
32
  "name": "Dependencias Federales"
33
33
  },
34
34
  "A": {
35
- "name": "Distrito Federal"
35
+ "name": "Distrito Capital"
36
36
  },
37
37
  "I": {
38
38
  "name": "Falcón"
@@ -40,6 +40,9 @@
40
40
  "J": {
41
41
  "name": "Guárico"
42
42
  },
43
+ "X": {
44
+ "name": "La Guaira"
45
+ },
43
46
  "K": {
44
47
  "name": "Lara"
45
48
  },
@@ -67,9 +70,6 @@
67
70
  "T": {
68
71
  "name": "Trujillo"
69
72
  },
70
- "X": {
71
- "name": "La Guaira"
72
- },
73
73
  "U": {
74
74
  "name": "Yaracuy"
75
75
  },
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "79": {
62
62
  "name": "Ho Chi Minh City",
63
- "local_name": "Thành phố Hồ Chí Minh"
63
+ "local_name": "Hồ Chí Minh"
64
64
  },
65
65
  "46": {
66
66
  "name": "Hue City",
@@ -8,9 +8,17 @@ module Addressing
8
8
  #
9
9
  # @example Get address format for Brazil
10
10
  # format = Addressing::AddressFormat.get('BR')
11
- # format.used_fields # => ["given_name", "family_name", ...]
12
- # format.required_fields # => ["address_line1", "locality", ...]
11
+ # format.used_fields # => ["given_name", "family_name", ...]
12
+ # format.required_fields # => ["address_line1", "locality", ...]
13
+ # format.subdivision_fields # => ["administrative_area", "locality"]
13
14
  class AddressFormat
15
+ # The subdivision fields, ordered from the top level down.
16
+ SUBDIVISION_FIELDS = [
17
+ AddressField::ADMINISTRATIVE_AREA,
18
+ AddressField::LOCALITY,
19
+ AddressField::DEPENDENT_LOCALITY
20
+ ].freeze
21
+
14
22
  class << self
15
23
  # Gets the address format for the provided country code.
16
24
  #
@@ -63,13 +71,28 @@ module Addressing
63
71
  administrative_area_type: "province",
64
72
  locality_type: "city",
65
73
  dependent_locality_type: "suburb",
66
- postal_code_type: "postal",
67
- subdivision_depth: 0
74
+ postal_code_type: "postal"
68
75
  }
69
76
  end
70
77
  end
71
78
 
72
- attr_reader :country_code, :locale, :format, :local_format, :required_fields, :uppercase_fields, :default_values, :administrative_area_type, :locality_type, :dependent_locality_type, :postal_code_type, :postal_code_pattern, :postal_code_prefix, :subdivision_depth
79
+ attr_reader :country_code, :locale, :format, :local_format, :required_fields, :uppercase_fields, :default_values, :administrative_area_type, :locality_type, :dependent_locality_type, :postal_code_type, :postal_code_pattern, :postal_code_prefix
80
+
81
+ # The subdivision fields for which there is predefined subdivision data.
82
+ #
83
+ # This is more precise than #used_subdivision_fields, which returns all
84
+ # subdivision fields used by the format regardless of whether data exists.
85
+ #
86
+ # @return [Array<String>] e.g. ["administrative_area", "locality"]
87
+ attr_reader :subdivision_fields
88
+
89
+ # The number of subdivision fields with predefined data used by the format.
90
+ #
91
+ # @deprecated Use #subdivision_fields instead.
92
+ # @return [Integer]
93
+ def subdivision_depth
94
+ (subdivision_fields & used_subdivision_fields).size
95
+ end
73
96
 
74
97
  def initialize(definition = {})
75
98
  # Validate the presence of required properties.
@@ -87,13 +110,16 @@ module Addressing
87
110
  uppercase_fields: [],
88
111
  default_values: {},
89
112
  postal_code_pattern: nil,
90
- postal_code_prefix: nil,
91
- subdivision_depth: 0
113
+ postal_code_prefix: nil
92
114
  }.merge(definition)
93
115
 
116
+ # Backwards compatibility: derive the subdivision fields from a depth.
117
+ definition[:subdivision_fields] ||= SUBDIVISION_FIELDS.first(definition[:subdivision_depth] || 0)
118
+
94
119
  AddressField.assert_all_exist(definition[:required_fields])
95
120
  AddressField.assert_all_exist(definition[:uppercase_fields])
96
121
  AddressField.assert_all_exist(definition[:default_values].keys)
122
+ AddressField.assert_all_exist(definition[:subdivision_fields])
97
123
 
98
124
  @country_code = definition[:country_code]
99
125
  @locale = definition[:locale]
@@ -102,7 +128,7 @@ module Addressing
102
128
  @required_fields = definition[:required_fields]
103
129
  @uppercase_fields = definition[:uppercase_fields]
104
130
  @default_values = definition[:default_values]
105
- @subdivision_depth = definition[:subdivision_depth]
131
+ @subdivision_fields = definition[:subdivision_fields]
106
132
 
107
133
  if used_fields.include?(AddressField::ADMINISTRATIVE_AREA)
108
134
  if definition[:administrative_area_type]
@@ -144,15 +170,11 @@ module Addressing
144
170
  end
145
171
 
146
172
  # Gets the list of used subdivision fields.
173
+ #
174
+ # Note that a country might use a subdivision field without having
175
+ # predefined subdivisions for it, see #subdivision_fields.
147
176
  def used_subdivision_fields
148
- fields = [
149
- AddressField::ADMINISTRATIVE_AREA,
150
- AddressField::LOCALITY,
151
- AddressField::DEPENDENT_LOCALITY
152
- ]
153
-
154
- # Remove fields not used by the format.
155
- fields & used_fields
177
+ SUBDIVISION_FIELDS & used_fields
156
178
  end
157
179
  end
158
180
 
@@ -4,6 +4,7 @@ module Addressing
4
4
  class DefaultFormatter
5
5
  DEFAULT_LOCALE = "en"
6
6
  FORMAT_PLACEHOLDER_PATTERN = /%[a-z1-9_]+/
7
+ FORMAT_SPLIT_PATTERN = /(#{FORMAT_PLACEHOLDER_PATTERN})/
7
8
  LEADING_TRAILING_PUNCTUATION_PATTERN = /\A[ \-,]+|[ \-,]+\z/
8
9
  MULTIPLE_SPACES_PATTERN = /\s\s+/
9
10
 
@@ -39,7 +40,7 @@ module Addressing
39
40
  view = render_view(view)
40
41
 
41
42
  replacements = view.map { |key, element| ["%#{key}", element] }.to_h
42
- output = format_string.gsub(FORMAT_PLACEHOLDER_PATTERN) { |m| replacements[m] }
43
+ output = insert_values(format_string, replacements)
43
44
  output = clean_output(output)
44
45
 
45
46
  if options[:html]
@@ -99,6 +100,39 @@ module Addressing
99
100
  end.join(" ")
100
101
  end
101
102
 
103
+ # Inserts the rendered address fields into the format string.
104
+ #
105
+ # Empty fields need special handling. When one falls between two values,
106
+ # keep the separator before it and discard the one after it.
107
+ def insert_values(format_string, replacements)
108
+ format_string.split("\n", -1).map do |line|
109
+ rendered = +""
110
+ separator = ""
111
+ skipped = false
112
+
113
+ line.split(FORMAT_SPLIT_PATTERN, -1).each do |part|
114
+ unless replacements.key?(part)
115
+ # A separator before an empty field usually belongs to
116
+ # the value that came before it, so keep that one.
117
+ separator = part unless skipped
118
+ next
119
+ end
120
+
121
+ if replacements[part].empty?
122
+ skipped = true
123
+ next
124
+ end
125
+
126
+ rendered << separator if !rendered.empty? || !skipped
127
+ rendered << replacements[part]
128
+ separator = ""
129
+ skipped = false
130
+ end
131
+
132
+ rendered
133
+ end.join("\n")
134
+ end
135
+
102
136
  # Removes empty lines, leading punctuation, excess whitespace.
103
137
  def clean_output(output)
104
138
  output.split("\n").map { |line| line.gsub(LEADING_TRAILING_PUNCTUATION_PATTERN, "").strip.gsub(MULTIPLE_SPACES_PATTERN, " ") }.reject(&:empty?).join("\n")
@@ -116,22 +150,18 @@ module Addressing
116
150
  AddressField.all.map { |_, field| [field, address.send(field)] }.to_h
117
151
  end
118
152
 
119
- # Resolves subdivision values to their display codes.
153
+ # Replaces the subdivision values with the codes of any predefined ones.
120
154
  def resolve_subdivision_values(values, address, address_format)
121
- subdivision_fields = address_format.used_subdivision_fields
155
+ parents = [address.country_code]
122
156
 
123
- # Replace the subdivision values with the names of any predefined ones.
124
- subdivision_fields.each_with_index.inject([{}, []]) do |(original_values, parents), (field, index)|
157
+ address_format.subdivision_fields.each do |field|
125
158
  # This level is empty, so there can be no sublevels.
126
- break if values[field].nil?
127
-
128
- parents << ((index > 0) ? original_values[subdivision_fields[index - 1]] : address.country_code)
159
+ break if values[field].nil? || values[field].empty?
129
160
 
130
161
  subdivision = Subdivision.get(values[field], parents)
131
162
  break if subdivision.nil?
132
163
 
133
- # Remember the original value so that it can be used for parents.
134
- original_values[field] = values[field]
164
+ parents << values[field]
135
165
 
136
166
  # Replace the value with the expected code.
137
167
  use_local_name = Locale.match_candidates(address.locale, subdivision.locale)
@@ -139,8 +169,6 @@ module Addressing
139
169
 
140
170
  # The current subdivision has no children, stop.
141
171
  break unless subdivision.children?
142
-
143
- [original_values, parents]
144
172
  end
145
173
  end
146
174
 
@@ -46,25 +46,24 @@ module Addressing
46
46
  end
47
47
 
48
48
  define_method :verify_subdivisions do |address, address_format, field_overrides|
49
- # No predefined subdivisions exist, nothing to validate against.
50
- return [] if address_format.subdivision_depth < 1
49
+ parents = [address_format.country_code]
50
+ subdivisions = []
51
51
 
52
- subdivisions, _parents = address_format.used_subdivision_fields.each_with_index.inject([[], []]) do |(subdivisions, parents), (field, index)|
52
+ address_format.subdivision_fields.each do |field|
53
+ value = address.send(field)
53
54
  # The field is empty or validation is disabled.
54
- break [subdivisions, parents] if address.send(field).blank? || field_overrides.hidden_fields.include?(field)
55
+ break if value.blank? || field_overrides.hidden_fields.include?(field)
55
56
 
56
- parents << ((index > 0) ? address.send(address_format.used_subdivision_fields[index - 1]) : address_format.country_code)
57
- subdivision = Subdivision.get(address.send(field), parents)
57
+ subdivision = Subdivision.get(value, parents)
58
58
  if subdivision.nil?
59
59
  errors.add(field, "should be valid")
60
- break [subdivisions, parents]
60
+ break
61
61
  end
62
62
 
63
+ parents << value
63
64
  subdivisions << subdivision
64
65
  # No predefined subdivisions below this level, stop here.
65
- break [subdivisions, parents] if subdivision.children.empty?
66
-
67
- [subdivisions, parents]
66
+ break unless subdivision.children?
68
67
  end
69
68
 
70
69
  subdivisions
@@ -58,8 +58,8 @@ module Addressing
58
58
  def has_data(parents)
59
59
  country_code = parents[0]
60
60
 
61
- depth = AddressFormat.get(country_code).subdivision_depth
62
- return false if depth == 0
61
+ subdivision_fields = AddressFormat.get(country_code).subdivision_fields
62
+ return false if subdivision_fields.empty?
63
63
 
64
64
  if parents.size > 1
65
65
  # After the first level it is possible for predefined subdivisions
@@ -74,8 +74,9 @@ module Addressing
74
74
  definition = @definitions[parent_group]["subdivisions"][parent_id]
75
75
  return !!definition["has_children"]
76
76
  else
77
- # The parent definition wasn't loaded previously, fallback to guessing based on depth.
78
- return parents.size <= depth
77
+ # The parent definition wasn't loaded previously, fallback to
78
+ # guessing based on the count of subdivision data fields.
79
+ return parents.size <= subdivision_fields.size
79
80
  end
80
81
  end
81
82
 
@@ -98,19 +99,30 @@ module Addressing
98
99
  filename = File.join(File.expand_path("../../../data/subdivision", __FILE__).to_s, "#{group}.json")
99
100
 
100
101
  if File.exist?(filename)
101
- raw_definition = File.read(filename)
102
- @definitions[group] = JSON.parse(raw_definition)
103
- @definitions[group] = process_definitions(@definitions[group])
102
+ @definitions[group] = process_definitions(parse_definitions(File.read(filename)))
104
103
  end
105
104
  end
106
105
 
107
106
  @definitions[group]
108
107
  end
109
108
 
109
+ # Parses a raw definition file.
110
+ #
111
+ # Malformed JSON is treated as if the file didn't exist.
112
+ def parse_definitions(raw_definition)
113
+ definitions = JSON.parse(raw_definition)
114
+ definitions.is_a?(Hash) ? definitions : {}
115
+ rescue JSON::ParserError
116
+ {}
117
+ end
118
+
110
119
  # Processes the loaded definitions.
111
120
  #
112
121
  # Adds keys and values that were removed from the JSON files for brevity.
113
122
  def process_definitions(definitions)
123
+ # Malformed definitions are treated as if they didn't exist.
124
+ return {} unless definitions["subdivisions"].is_a?(Hash)
125
+
114
126
  definitions["subdivisions"].each do |id, definition|
115
127
  # Add common keys from the root level.
116
128
  definition["country_code"] = definitions["country_code"]
@@ -124,6 +136,12 @@ module Addressing
124
136
  definition["name"] = id
125
137
  end
126
138
 
139
+ # The local_name value is only specified if it doesn't match
140
+ # the name one.
141
+ if definitions.key?("locale") && !definition.key?("local_name")
142
+ definition["local_name"] = definition["name"]
143
+ end
144
+
127
145
  # The code and local_code values are only specified if they
128
146
  # don't match the name and local_name ones.
129
147
  if !definition.key?("code") && definition.key?("name")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Addressing
4
- VERSION = "2.0.1"
4
+ VERSION = "2.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: addressing
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin van der Vleuten
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-27 00:00:00.000000000 Z
11
+ date: 2026-09-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: robinvdvleuten@gmail.com
@@ -617,6 +617,8 @@ files:
617
617
  - data/subdivision/EG.json
618
618
  - data/subdivision/ES.json
619
619
  - data/subdivision/FM.json
620
+ - data/subdivision/GB.json
621
+ - data/subdivision/GT.json
620
622
  - data/subdivision/HK-2705954f46a43ef0ad522412afe6ed6ee8196fe4.json
621
623
  - data/subdivision/HK-29ceaf41d01206ba481b188116cbb302b82663cf.json
622
624
  - data/subdivision/HK-ff51a84233701e6a1d18f70acf4b3f909e4c6a09.json