addressing 2.0.1 → 2.2.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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +223 -111
  4. data/data/UPSTREAM_VERSION +1 -0
  5. data/data/address_formats.json +2256 -206
  6. data/data/countries.json +1282 -0
  7. data/data/locale.json +222 -0
  8. data/data/subdivision/AE.json +1 -1
  9. data/data/subdivision/BR-AC.json +22 -22
  10. data/data/subdivision/BR-AL.json +102 -102
  11. data/data/subdivision/BR-AM.json +62 -62
  12. data/data/subdivision/BR-AP.json +16 -16
  13. data/data/subdivision/BR-BA.json +417 -417
  14. data/data/subdivision/BR-CE.json +184 -184
  15. data/data/subdivision/BR-DF.json +1 -1
  16. data/data/subdivision/BR-ES.json +79 -79
  17. data/data/subdivision/BR-GO.json +246 -246
  18. data/data/subdivision/BR-MA.json +217 -217
  19. data/data/subdivision/BR-MG.json +853 -853
  20. data/data/subdivision/BR-MS.json +78 -78
  21. data/data/subdivision/BR-MT.json +141 -141
  22. data/data/subdivision/BR-PA.json +144 -144
  23. data/data/subdivision/BR-PB.json +223 -223
  24. data/data/subdivision/BR-PE.json +185 -185
  25. data/data/subdivision/BR-PI.json +223 -223
  26. data/data/subdivision/BR-PR.json +400 -400
  27. data/data/subdivision/BR-RJ.json +93 -93
  28. data/data/subdivision/BR-RN.json +166 -166
  29. data/data/subdivision/BR-RO.json +52 -52
  30. data/data/subdivision/BR-RR.json +15 -15
  31. data/data/subdivision/BR-RS.json +497 -497
  32. data/data/subdivision/BR-SC.json +295 -295
  33. data/data/subdivision/BR-SE.json +75 -75
  34. data/data/subdivision/BR-SP.json +645 -645
  35. data/data/subdivision/BR-TO.json +139 -139
  36. data/data/subdivision/CA.json +6 -12
  37. data/data/subdivision/CL-AI.json +10 -10
  38. data/data/subdivision/CL-AN.json +9 -9
  39. data/data/subdivision/CL-AP.json +4 -4
  40. data/data/subdivision/CL-AR.json +32 -32
  41. data/data/subdivision/CL-AT.json +9 -9
  42. data/data/subdivision/CL-BI.json +34 -34
  43. data/data/subdivision/CL-CO.json +15 -15
  44. data/data/subdivision/CL-LI.json +33 -33
  45. data/data/subdivision/CL-LL.json +30 -30
  46. data/data/subdivision/CL-LR.json +12 -12
  47. data/data/subdivision/CL-MA.json +11 -11
  48. data/data/subdivision/CL-ML.json +30 -30
  49. data/data/subdivision/CL-NB.json +21 -21
  50. data/data/subdivision/CL-RM.json +52 -52
  51. data/data/subdivision/CL-TA.json +7 -7
  52. data/data/subdivision/CL-VS.json +38 -38
  53. data/data/subdivision/CO.json +1 -1
  54. data/data/subdivision/CV.json +9 -9
  55. data/data/subdivision/GB.json +675 -0
  56. data/data/subdivision/GT.json +71 -0
  57. data/data/subdivision/IT.json +13 -1
  58. data/data/subdivision/KY.json +3 -3
  59. data/data/subdivision/NG.json +1 -1
  60. data/data/subdivision/RU.json +4 -4
  61. data/data/subdivision/TH.json +7 -7
  62. data/data/subdivision/TR.json +1 -1
  63. data/data/subdivision/TV.json +1 -1
  64. data/data/subdivision/VE.json +4 -4
  65. data/data/subdivision/VN.json +1 -1
  66. data/lib/addressing/address.rb +7 -3
  67. data/lib/addressing/address_format.rb +93 -97
  68. data/lib/addressing/address_validator.rb +98 -0
  69. data/lib/addressing/blank.rb +32 -0
  70. data/lib/addressing/country.rb +28 -314
  71. data/lib/addressing/data_source.rb +75 -0
  72. data/lib/addressing/default_formatter.rb +61 -33
  73. data/lib/addressing/enum.rb +2 -6
  74. data/lib/addressing/field_violation.rb +17 -0
  75. data/lib/addressing/lazy_subdivisions.rb +16 -15
  76. data/lib/addressing/locale.rb +20 -269
  77. data/lib/addressing/model.rb +20 -79
  78. data/lib/addressing/postal_label_formatter.rb +16 -15
  79. data/lib/addressing/subdivision.rb +121 -106
  80. data/lib/addressing/version.rb +1 -1
  81. data/lib/addressing.rb +6 -1
  82. metadata +11 -3
  83. data/data/address_formats.dump +0 -0
@@ -17,26 +17,6 @@ module Addressing
17
17
  # list = Addressing::Country.list('en')
18
18
  class Country
19
19
  class << self
20
- AVAILABLE_LOCALES = [
21
- "af", "ak", "am", "ar", "ar-LY", "ar-SA", "as", "az", "ba", "be", "bg",
22
- "bn", "bn-IN", "bs", "ca", "chr", "cs", "cv", "cy", "da", "de",
23
- "de-AT", "de-CH", "dsb", "el", "el-polyton", "en", "en-001", "en-AU",
24
- "en-CA", "en-EE", "en-GE", "en-LT", "en-LV", "en-UA", "es", "es-419",
25
- "es-AR", "es-BO", "es-CL", "es-CO", "es-CR", "es-DO", "es-EC", "es-GT",
26
- "es-HN", "es-MX", "es-NI", "es-PA", "es-PE", "es-PR", "es-PY", "es-SV",
27
- "es-US", "es-VE", "et", "eu", "fa", "fa-AF", "fi", "fil", "fr",
28
- "fr-BE", "fr-CA", "ga", "gd", "gl", "gu", "he", "hi", "hi-Latn", "hr",
29
- "hsb", "hu", "hy", "id", "ig", "is", "it", "ja", "ka", "kk", "kk-Arab",
30
- "km", "ko", "ko-KP", "kok", "ky", "lo", "lt", "lv", "mk", "ml", "mn",
31
- "mr", "ms", "my", "ne", "nl", "nn", "no", "or", "pa", "pl", "ps",
32
- "ps-PK", "pt", "pt-PT", "qu", "rm", "ro", "ro-MD", "ru", "ru-UA",
33
- "shn", "si", "sk", "sl", "so", "sq", "sr", "sr-Cyrl-BA", "sr-Cyrl-ME",
34
- "sr-Cyrl-XK", "sr-Latn", "sr-Latn-BA", "sr-Latn-ME", "sr-Latn-XK",
35
- "sv", "sw", "sw-CD", "sw-KE", "ta", "te", "th", "tk", "tr", "uk", "ur",
36
- "ur-IN", "uz", "vi", "yue", "yue-Hans", "zh", "zh-Hant", "zh-Hant-HK",
37
- "zu"
38
- ]
39
-
40
20
  # Gets a Country instance for the provided country code.
41
21
  #
42
22
  # @param country_code [String] ISO 3166-1 alpha-2 country code
@@ -49,17 +29,9 @@ module Addressing
49
29
 
50
30
  raise UnknownCountryError.new(country_code) unless base_definitions.key?(country_code)
51
31
 
52
- locale = Locale.resolve(AVAILABLE_LOCALES, locale, fallback_locale)
53
- definitions = load_definitions(locale)
32
+ locale = Locale.resolve(available_locales, locale, fallback_locale)
54
33
 
55
- new(
56
- country_code: country_code,
57
- name: definitions[country_code],
58
- three_letter_code: base_definitions[country_code][0],
59
- numeric_code: base_definitions[country_code][1],
60
- currency_code: base_definitions[country_code][2],
61
- locale: locale
62
- )
34
+ build(country_code, load_definitions(locale)[country_code], locale)
63
35
  end
64
36
 
65
37
  # Gets all Country instances.
@@ -68,21 +40,11 @@ module Addressing
68
40
  # @param fallback_locale [String] Fallback locale if requested locale is unavailable
69
41
  # @return [Hash<String, Country>] Hash of country code => Country instance
70
42
  def all(locale = "en", fallback_locale = "en")
71
- locale = Locale.resolve(AVAILABLE_LOCALES, locale, fallback_locale)
72
- definitions = load_definitions(locale)
73
-
74
- definitions.map do |country_code, country_name|
75
- country = new(
76
- country_code: country_code,
77
- name: country_name,
78
- three_letter_code: base_definitions[country_code][0],
79
- numeric_code: base_definitions[country_code][1],
80
- currency_code: base_definitions[country_code][2],
81
- locale: locale
82
- )
43
+ locale = Locale.resolve(available_locales, locale, fallback_locale)
83
44
 
84
- [country_code, country]
85
- end.to_h
45
+ load_definitions(locale).to_h do |country_code, country_name|
46
+ [country_code, build(country_code, country_name, locale)]
47
+ end
86
48
  end
87
49
 
88
50
  # Gets a list of country codes and names.
@@ -91,289 +53,41 @@ module Addressing
91
53
  # @param fallback_locale [String] Fallback locale if requested locale is unavailable
92
54
  # @return [Hash<String, String>] Hash of country code => country name
93
55
  def list(locale = "en", fallback_locale = "en")
94
- locale = Locale.resolve(AVAILABLE_LOCALES, locale, fallback_locale)
95
- definitions = load_definitions(locale)
56
+ locale = Locale.resolve(available_locales, locale, fallback_locale)
96
57
 
97
- definitions.map do |country_code, country_name|
98
- [country_code, country_name]
99
- end.to_h
58
+ load_definitions(locale).dup
100
59
  end
101
60
 
102
61
  protected
103
62
 
63
+ def build(country_code, name, locale)
64
+ base_definition = base_definitions[country_code]
65
+
66
+ new(
67
+ country_code: country_code,
68
+ name: name,
69
+ three_letter_code: base_definition["three_letter_code"],
70
+ numeric_code: base_definition["numeric_code"],
71
+ currency_code: base_definition["currency_code"],
72
+ locale: locale
73
+ )
74
+ end
75
+
104
76
  # Loads the country definitions for the provided locale.
105
77
  def load_definitions(locale)
106
- @definitions ||= {}
107
- unless @definitions.key?(locale)
108
- filename = File.join(File.expand_path("../../../data/country", __FILE__).to_s, "#{locale}.json")
109
- @definitions[locale] = JSON.parse(File.read(filename))
110
- end
78
+ Addressing.data_source.fetch("country/#{locale}")
79
+ end
111
80
 
112
- @definitions[locale]
81
+ # Gets the locales that have country names, one data file each.
82
+ def available_locales
83
+ Addressing.data_source.names("country")
113
84
  end
114
85
 
115
86
  # Gets the base country definitions.
116
87
  #
117
- # Contains data common to all locales: three letter code, numeric code.
88
+ # Contains data common to all locales: three letter code, numeric code, currency code.
118
89
  def base_definitions
119
- @base_definitions ||= {
120
- "AC" => ["ASC", nil, "SHP"],
121
- "AD" => ["AND", "020", "EUR"],
122
- "AE" => ["ARE", "784", "AED"],
123
- "AF" => ["AFG", "004", "AFN"],
124
- "AG" => ["ATG", "028", "XCD"],
125
- "AI" => ["AIA", "660", "XCD"],
126
- "AL" => ["ALB", "008", "ALL"],
127
- "AM" => ["ARM", "051", "AMD"],
128
- "AO" => ["AGO", "024", "AOA"],
129
- "AQ" => ["ATA", "010", nil],
130
- "AR" => ["ARG", "032", "ARS"],
131
- "AS" => ["ASM", "016", "USD"],
132
- "AT" => ["AUT", "040", "EUR"],
133
- "AU" => ["AUS", "036", "AUD"],
134
- "AW" => ["ABW", "533", "AWG"],
135
- "AX" => ["ALA", "248", "EUR"],
136
- "AZ" => ["AZE", "031", "AZN"],
137
- "BA" => ["BIH", "070", "BAM"],
138
- "BB" => ["BRB", "052", "BBD"],
139
- "BD" => ["BGD", "050", "BDT"],
140
- "BE" => ["BEL", "056", "EUR"],
141
- "BF" => ["BFA", "854", "XOF"],
142
- "BG" => ["BGR", "100", "EUR"],
143
- "BH" => ["BHR", "048", "BHD"],
144
- "BI" => ["BDI", "108", "BIF"],
145
- "BJ" => ["BEN", "204", "XOF"],
146
- "BL" => ["BLM", "652", "EUR"],
147
- "BM" => ["BMU", "060", "BMD"],
148
- "BN" => ["BRN", "096", "BND"],
149
- "BO" => ["BOL", "068", "BOB"],
150
- "BQ" => ["BES", "535", "USD"],
151
- "BR" => ["BRA", "076", "BRL"],
152
- "BS" => ["BHS", "044", "BSD"],
153
- "BT" => ["BTN", "064", "BTN"],
154
- "BV" => ["BVT", "074", "NOK"],
155
- "BW" => ["BWA", "072", "BWP"],
156
- "BY" => ["BLR", "112", "BYN"],
157
- "BZ" => ["BLZ", "084", "BZD"],
158
- "CA" => ["CAN", "124", "CAD"],
159
- "CC" => ["CCK", "166", "AUD"],
160
- "CD" => ["COD", "180", "CDF"],
161
- "CF" => ["CAF", "140", "XAF"],
162
- "CG" => ["COG", "178", "XAF"],
163
- "CH" => ["CHE", "756", "CHF"],
164
- "CI" => ["CIV", "384", "XOF"],
165
- "CK" => ["COK", "184", "NZD"],
166
- "CL" => ["CHL", "152", "CLP"],
167
- "CM" => ["CMR", "120", "XAF"],
168
- "CN" => ["CHN", "156", "CNY"],
169
- "CO" => ["COL", "170", "COP"],
170
- "CP" => ["CPT", nil, nil],
171
- "CR" => ["CRI", "188", "CRC"],
172
- "CU" => ["CUB", "192", "CUP"],
173
- "CV" => ["CPV", "132", "CVE"],
174
- "CW" => ["CUW", "531", "XCG"],
175
- "CX" => ["CXR", "162", "AUD"],
176
- "CY" => ["CYP", "196", "EUR"],
177
- "CZ" => ["CZE", "203", "CZK"],
178
- "DE" => ["DEU", "276", "EUR"],
179
- "DG" => ["DGA", nil, "USD"],
180
- "DJ" => ["DJI", "262", "DJF"],
181
- "DK" => ["DNK", "208", "DKK"],
182
- "DM" => ["DMA", "212", "XCD"],
183
- "DO" => ["DOM", "214", "DOP"],
184
- "DZ" => ["DZA", "012", "DZD"],
185
- "EA" => [nil, nil, "EUR"],
186
- "EC" => ["ECU", "218", "USD"],
187
- "EE" => ["EST", "233", "EUR"],
188
- "EG" => ["EGY", "818", "EGP"],
189
- "EH" => ["ESH", "732", "MAD"],
190
- "ER" => ["ERI", "232", "ERN"],
191
- "ES" => ["ESP", "724", "EUR"],
192
- "ET" => ["ETH", "231", "ETB"],
193
- "FI" => ["FIN", "246", "EUR"],
194
- "FJ" => ["FJI", "242", "FJD"],
195
- "FK" => ["FLK", "238", "FKP"],
196
- "FM" => ["FSM", "583", "USD"],
197
- "FO" => ["FRO", "234", "DKK"],
198
- "FR" => ["FRA", "250", "EUR"],
199
- "GA" => ["GAB", "266", "XAF"],
200
- "GB" => ["GBR", "826", "GBP"],
201
- "GD" => ["GRD", "308", "XCD"],
202
- "GE" => ["GEO", "268", "GEL"],
203
- "GF" => ["GUF", "254", "EUR"],
204
- "GG" => ["GGY", "831", "GBP"],
205
- "GH" => ["GHA", "288", "GHS"],
206
- "GI" => ["GIB", "292", "GIP"],
207
- "GL" => ["GRL", "304", "DKK"],
208
- "GM" => ["GMB", "270", "GMD"],
209
- "GN" => ["GIN", "324", "GNF"],
210
- "GP" => ["GLP", "312", "EUR"],
211
- "GQ" => ["GNQ", "226", "XAF"],
212
- "GR" => ["GRC", "300", "EUR"],
213
- "GS" => ["SGS", "239", "GBP"],
214
- "GT" => ["GTM", "320", "GTQ"],
215
- "GU" => ["GUM", "316", "USD"],
216
- "GW" => ["GNB", "624", "XOF"],
217
- "GY" => ["GUY", "328", "GYD"],
218
- "HK" => ["HKG", "344", "HKD"],
219
- "HM" => ["HMD", "334", "AUD"],
220
- "HN" => ["HND", "340", "HNL"],
221
- "HR" => ["HRV", "191", "EUR"],
222
- "HT" => ["HTI", "332", "USD"],
223
- "HU" => ["HUN", "348", "HUF"],
224
- "IC" => [nil, nil, "EUR"],
225
- "ID" => ["IDN", "360", "IDR"],
226
- "IE" => ["IRL", "372", "EUR"],
227
- "IL" => ["ISR", "376", "ILS"],
228
- "IM" => ["IMN", "833", "GBP"],
229
- "IN" => ["IND", "356", "INR"],
230
- "IO" => ["IOT", "086", "USD"],
231
- "IQ" => ["IRQ", "368", "IQD"],
232
- "IR" => ["IRN", "364", "IRR"],
233
- "IS" => ["ISL", "352", "ISK"],
234
- "IT" => ["ITA", "380", "EUR"],
235
- "JE" => ["JEY", "832", "GBP"],
236
- "JM" => ["JAM", "388", "JMD"],
237
- "JO" => ["JOR", "400", "JOD"],
238
- "JP" => ["JPN", "392", "JPY"],
239
- "KE" => ["KEN", "404", "KES"],
240
- "KG" => ["KGZ", "417", "KGS"],
241
- "KH" => ["KHM", "116", "KHR"],
242
- "KI" => ["KIR", "296", "AUD"],
243
- "KM" => ["COM", "174", "KMF"],
244
- "KN" => ["KNA", "659", "XCD"],
245
- "KP" => ["PRK", "408", "KPW"],
246
- "KR" => ["KOR", "410", "KRW"],
247
- "KW" => ["KWT", "414", "KWD"],
248
- "KY" => ["CYM", "136", "KYD"],
249
- "KZ" => ["KAZ", "398", "KZT"],
250
- "LA" => ["LAO", "418", "LAK"],
251
- "LB" => ["LBN", "422", "LBP"],
252
- "LC" => ["LCA", "662", "XCD"],
253
- "LI" => ["LIE", "438", "CHF"],
254
- "LK" => ["LKA", "144", "LKR"],
255
- "LR" => ["LBR", "430", "LRD"],
256
- "LS" => ["LSO", "426", "LSL"],
257
- "LT" => ["LTU", "440", "EUR"],
258
- "LU" => ["LUX", "442", "EUR"],
259
- "LV" => ["LVA", "428", "EUR"],
260
- "LY" => ["LBY", "434", "LYD"],
261
- "MA" => ["MAR", "504", "MAD"],
262
- "MC" => ["MCO", "492", "EUR"],
263
- "MD" => ["MDA", "498", "MDL"],
264
- "ME" => ["MNE", "499", "EUR"],
265
- "MF" => ["MAF", "663", "EUR"],
266
- "MG" => ["MDG", "450", "MGA"],
267
- "MH" => ["MHL", "584", "USD"],
268
- "MK" => ["MKD", "807", "MKD"],
269
- "ML" => ["MLI", "466", nil],
270
- "MM" => ["MMR", "104", "MMK"],
271
- "MN" => ["MNG", "496", "MNT"],
272
- "MO" => ["MAC", "446", "MOP"],
273
- "MP" => ["MNP", "580", "USD"],
274
- "MQ" => ["MTQ", "474", "EUR"],
275
- "MR" => ["MRT", "478", "MRU"],
276
- "MS" => ["MSR", "500", "XCD"],
277
- "MT" => ["MLT", "470", "EUR"],
278
- "MU" => ["MUS", "480", "MUR"],
279
- "MV" => ["MDV", "462", "MVR"],
280
- "MW" => ["MWI", "454", "MWK"],
281
- "MX" => ["MEX", "484", "MXN"],
282
- "MY" => ["MYS", "458", "MYR"],
283
- "MZ" => ["MOZ", "508", "MZN"],
284
- "NA" => ["NAM", "516", "NAD"],
285
- "NC" => ["NCL", "540", "XPF"],
286
- "NE" => ["NER", "562", "XOF"],
287
- "NF" => ["NFK", "574", "AUD"],
288
- "NG" => ["NGA", "566", "NGN"],
289
- "NI" => ["NIC", "558", "NIO"],
290
- "NL" => ["NLD", "528", "EUR"],
291
- "NO" => ["NOR", "578", "NOK"],
292
- "NP" => ["NPL", "524", "NPR"],
293
- "NR" => ["NRU", "520", "AUD"],
294
- "NU" => ["NIU", "570", "NZD"],
295
- "NZ" => ["NZL", "554", "NZD"],
296
- "OM" => ["OMN", "512", "OMR"],
297
- "PA" => ["PAN", "591", "USD"],
298
- "PE" => ["PER", "604", "PEN"],
299
- "PF" => ["PYF", "258", "XPF"],
300
- "PG" => ["PNG", "598", "PGK"],
301
- "PH" => ["PHL", "608", "PHP"],
302
- "PK" => ["PAK", "586", "PKR"],
303
- "PL" => ["POL", "616", "PLN"],
304
- "PM" => ["SPM", "666", "EUR"],
305
- "PN" => ["PCN", "612", "NZD"],
306
- "PR" => ["PRI", "630", "USD"],
307
- "PS" => ["PSE", "275", "ILS"],
308
- "PT" => ["PRT", "620", "EUR"],
309
- "PW" => ["PLW", "585", "USD"],
310
- "PY" => ["PRY", "600", "PYG"],
311
- "QA" => ["QAT", "634", "QAR"],
312
- "RE" => ["REU", "638", "EUR"],
313
- "RO" => ["ROU", "642", "RON"],
314
- "RS" => ["SRB", "688", "RSD"],
315
- "RU" => ["RUS", "643", "RUB"],
316
- "RW" => ["RWA", "646", "RWF"],
317
- "SA" => ["SAU", "682", "SAR"],
318
- "SB" => ["SLB", "090", "SBD"],
319
- "SC" => ["SYC", "690", "SCR"],
320
- "SD" => ["SDN", "729", "SDG"],
321
- "SE" => ["SWE", "752", "SEK"],
322
- "SG" => ["SGP", "702", "SGD"],
323
- "SH" => ["SHN", "654", "SHP"],
324
- "SI" => ["SVN", "705", "EUR"],
325
- "SJ" => ["SJM", "744", "NOK"],
326
- "SK" => ["SVK", "703", "EUR"],
327
- "SL" => ["SLE", "694", "SLE"],
328
- "SM" => ["SMR", "674", "EUR"],
329
- "SN" => ["SEN", "686", "XOF"],
330
- "SO" => ["SOM", "706", "SOS"],
331
- "SR" => ["SUR", "740", "SRD"],
332
- "SS" => ["SSD", "728", "SSP"],
333
- "ST" => ["STP", "678", "STN"],
334
- "SV" => ["SLV", "222", "USD"],
335
- "SX" => ["SXM", "534", "XCG"],
336
- "SY" => ["SYR", "760", "SYP"],
337
- "SZ" => ["SWZ", "748", "SZL"],
338
- "TA" => ["TAA", nil, "GBP"],
339
- "TC" => ["TCA", "796", "USD"],
340
- "TD" => ["TCD", "148", "XAF"],
341
- "TF" => ["ATF", "260", "EUR"],
342
- "TG" => ["TGO", "768", "XOF"],
343
- "TH" => ["THA", "764", "THB"],
344
- "TJ" => ["TJK", "762", "TJS"],
345
- "TK" => ["TKL", "772", "NZD"],
346
- "TL" => ["TLS", "626", "USD"],
347
- "TM" => ["TKM", "795", "TMT"],
348
- "TN" => ["TUN", "788", "TND"],
349
- "TO" => ["TON", "776", "TOP"],
350
- "TR" => ["TUR", "792", "TRY"],
351
- "TT" => ["TTO", "780", "TTD"],
352
- "TV" => ["TUV", "798", "AUD"],
353
- "TW" => ["TWN", "158", "TWD"],
354
- "TZ" => ["TZA", "834", "TZS"],
355
- "UA" => ["UKR", "804", "UAH"],
356
- "UG" => ["UGA", "800", "UGX"],
357
- "UM" => ["UMI", "581", "USD"],
358
- "US" => ["USA", "840", "USD"],
359
- "UY" => ["URY", "858", "UYU"],
360
- "UZ" => ["UZB", "860", "UZS"],
361
- "VA" => ["VAT", "336", "EUR"],
362
- "VC" => ["VCT", "670", "XCD"],
363
- "VE" => ["VEN", "862", "VES"],
364
- "VG" => ["VGB", "092", "USD"],
365
- "VI" => ["VIR", "850", "USD"],
366
- "VN" => ["VNM", "704", "VND"],
367
- "VU" => ["VUT", "548", "VUV"],
368
- "WF" => ["WLF", "876", "XPF"],
369
- "WS" => ["WSM", "882", "WST"],
370
- "XK" => ["XKK", "983", "EUR"],
371
- "YE" => ["YEM", "887", "YER"],
372
- "YT" => ["MYT", "175", "EUR"],
373
- "ZA" => ["ZAF", "710", "ZAR"],
374
- "ZM" => ["ZMB", "894", "ZMW"],
375
- "ZW" => ["ZWE", "716", "ZWG"]
376
- }
90
+ Addressing.data_source.fetch("countries")
377
91
  end
378
92
  end
379
93
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Addressing
4
+ class << self
5
+ # The data source that the reference data is read from.
6
+ #
7
+ # @api private
8
+ attr_writer :data_source
9
+
10
+ # @api private
11
+ # @return [DataSource]
12
+ def data_source
13
+ @data_source ||= DataSource.new(DataSource::DEFAULT_DIR)
14
+ end
15
+ end
16
+
17
+ # Reads the reference data: the JSON files that the data sync writes.
18
+ #
19
+ # Each instance has its own cache, so a new instance reads the files again.
20
+ #
21
+ # @api private
22
+ class DataSource
23
+ # The data directory that ships with the gem.
24
+ DEFAULT_DIR = File.expand_path("../../data", __dir__)
25
+
26
+ def initialize(dir)
27
+ @dir = dir
28
+ @cache = {}
29
+ end
30
+
31
+ # Gets the dataset with the provided name.
32
+ #
33
+ # The parsed JSON is passed to the block, and the result of the block is
34
+ # cached, so each dataset is read and processed once per data source.
35
+ # A missing dataset gives nil, which is cached as well. Malformed JSON
36
+ # raises, because the data files are never edited by hand.
37
+ #
38
+ # @param name [String] Path relative to the data directory, without extension (e.g. "country/en")
39
+ # @yieldparam json [Hash, Array] The parsed dataset
40
+ # @return [Object, nil] The result of the block, or the parsed dataset without a block
41
+ # @raise [JSON::ParserError] if the dataset is malformed
42
+ def fetch(name)
43
+ return @cache[name] if @cache.key?(name)
44
+
45
+ dir, basename = File.split(name)
46
+ return @cache[name] = nil unless filenames(dir).include?("#{basename}.json")
47
+
48
+ json = JSON.parse(File.read(File.join(@dir, "#{name}.json"), encoding: "UTF-8"))
49
+ @cache[name] = block_given? ? yield(json) : json
50
+ end
51
+
52
+ # Gets the names of the datasets in a directory, without the directory.
53
+ #
54
+ # @param dir [String] Directory relative to the data directory (e.g. "country")
55
+ # @return [Array<String>] Sorted names (e.g. ["af", "ak", ...])
56
+ def names(dir)
57
+ (@names ||= {})[dir] ||= filenames(dir).filter_map { |filename| File.basename(filename, ".json") if filename.end_with?(".json") }.sort.freeze
58
+ end
59
+
60
+ private
61
+
62
+ # Gets the file names in a directory of the data directory.
63
+ #
64
+ # A dataset exists only when its name matches a file name exactly, so
65
+ # that a lookup gives the same result on a case-insensitive file system.
66
+ # The listing is cached like the datasets, so a file added later is not
67
+ # seen by this instance.
68
+ def filenames(dir)
69
+ (@filenames ||= {})[dir] ||= begin
70
+ path = File.join(@dir, dir)
71
+ Dir.exist?(path) ? Dir.children(path).to_set : Set.new
72
+ end
73
+ end
74
+ end
75
+ end
@@ -4,8 +4,10 @@ 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+/
10
+ HTML_TAG_PATTERN = /\A[a-z][a-z0-9-]*\z/i
9
11
 
10
12
  DEFAULT_OPTIONS = {
11
13
  locale: DEFAULT_LOCALE,
@@ -22,9 +24,8 @@ module Addressing
22
24
  end
23
25
 
24
26
  def format(address, options = {})
25
- assert_options(options)
26
-
27
27
  options = @default_options.merge(options)
28
+ assert_options(options)
28
29
  address_format = AddressFormat.get(address.country_code)
29
30
 
30
31
  # Add the country to the bottom or the top of the format string,
@@ -38,8 +39,8 @@ module Addressing
38
39
  view = build_view(address, address_format, options)
39
40
  view = render_view(view)
40
41
 
41
- replacements = view.map { |key, element| ["%#{key}", element] }.to_h
42
- output = format_string.gsub(FORMAT_PLACEHOLDER_PATTERN) { |m| replacements[m] }
42
+ replacements = view.transform_keys { |key| "%#{key}" }
43
+ output = insert_values(format_string, replacements)
43
44
  output = clean_output(output)
44
45
 
45
46
  if options[:html]
@@ -56,12 +57,12 @@ module Addressing
56
57
  # Builds the view for the given address.
57
58
  def build_view(address, address_format, options)
58
59
  countries = country_list(options[:locale])
59
- values = values(address, address_format).merge({"country" => countries.key?(address.country_code) ? countries[address.country_code] : address.country_code})
60
+ values = values(address, address_format).merge("country" => countries.fetch(address.country_code, address.country_code))
60
61
  used_fields = address_format.used_fields + ["country"]
61
62
 
62
- used_fields.map do |field|
63
+ used_fields.to_h do |field|
63
64
  [field, {html: options[:html], html_tag: "span", html_attributes: {class: field.tr("_", "-")}, value: values[field]}]
64
- end.to_h
65
+ end
65
66
  end
66
67
 
67
68
  # Gets the country list for a locale, with caching.
@@ -71,16 +72,16 @@ module Addressing
71
72
 
72
73
  # Renders the given view.
73
74
  def render_view(view)
74
- view.map do |key, element|
75
- next [key, ""] if element[:value].empty?
75
+ view.transform_values do |element|
76
+ next "" if element[:value].empty?
76
77
 
77
78
  if element[:html]
78
79
  element[:value] = CGI.escapeHTML(element[:value])
79
- next [key, render_html_element(element)]
80
+ next render_html_element(element)
80
81
  end
81
82
 
82
- [key, element[:value].gsub(/<\/?[^>]*>/, "")]
83
- end.to_h
83
+ element[:value].gsub(/<\/?[^>]*>/, "")
84
+ end
84
85
  end
85
86
 
86
87
  def render_html_element(element)
@@ -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")
@@ -113,42 +147,27 @@ module Addressing
113
147
 
114
148
  # Extracts all address field values.
115
149
  def extract_address_values(address)
116
- AddressField.all.map { |_, field| [field, address.send(field)] }.to_h
150
+ AddressField.all.values.to_h { |field| [field, address.send(field)] }
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
+ fields = address_format.subdivision_fields
156
+ chain = Subdivision.chain(address.country_code, values.values_at(*fields))
122
157
 
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)|
125
- # 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)
129
-
130
- subdivision = Subdivision.get(values[field], parents)
158
+ fields.zip(chain.subdivisions) do |field, subdivision|
131
159
  break if subdivision.nil?
132
160
 
133
- # Remember the original value so that it can be used for parents.
134
- original_values[field] = values[field]
135
-
136
161
  # Replace the value with the expected code.
137
162
  use_local_name = Locale.match_candidates(address.locale, subdivision.locale)
138
163
  values[field] = use_local_name ? subdivision.local_code : subdivision.code
139
-
140
- # The current subdivision has no children, stop.
141
- break unless subdivision.children?
142
-
143
- [original_values, parents]
144
164
  end
145
165
  end
146
166
 
147
- private
148
-
149
167
  # Validates the provided options.
150
168
  #
151
169
  # Ensures the absence of unknown keys, correct data types and values.
170
+ # A subclass with more options extends this and calls super.
152
171
  def assert_options(options)
153
172
  options.each do |option, value|
154
173
  unless self.class::DEFAULT_OPTIONS.key?(option)
@@ -163,6 +182,15 @@ module Addressing
163
182
  if options.key?(:html_attributes) && !options[:html_attributes].is_a?(Hash)
164
183
  raise ArgumentError, "The option `html_attributes` must be a hash."
165
184
  end
185
+
186
+ # The tag is written into the markup as is, so only allow a tag name.
187
+ if options.key?(:html_tag) && !HTML_TAG_PATTERN.match?(options[:html_tag].to_s)
188
+ raise ArgumentError, "The option `html_tag` must be an HTML tag name."
189
+ end
190
+
191
+ if options.key?(:locale) && !options[:locale].is_a?(String)
192
+ raise ArgumentError, "The option `locale` must be a string."
193
+ end
166
194
  end
167
195
  end
168
196
  end
@@ -5,12 +5,8 @@ module Addressing
5
5
  class << self
6
6
  # Gets all available values.
7
7
  def all
8
- @values ||= {}
9
- if !@values.key?(name)
10
- @values[name] = constants.map { |constant| [constant, const_get(constant)] }.to_h
11
- end
12
-
13
- @values[name]
8
+ # Each subclass has its own @all.
9
+ @all ||= constants.to_h { |constant| [constant, const_get(constant)] }
14
10
  end
15
11
 
16
12
  # Gets the key of the provided value.