address_concern 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,784 @@
1
+ require_relative '../../../lib/core_extensions/hash/reorder'
2
+ using Hash::Reorder
3
+
4
+ require_relative '../../../lib/core_extensions/string/cleanlines'
5
+ using String::Cleanlines
6
+
7
+ require_relative 'inspect_base'
8
+ require_relative 'attributes_slice'
9
+
10
+ module AddressConcern
11
+ module Address
12
+ module Base
13
+ extend ActiveSupport::Concern
14
+
15
+ # These (Base) class methods are added to ActiveRecord::Base so that they will be available from _any_
16
+ # model class. Unlike the main AddressConcern::Address methods which are only included _after_
17
+ # you call acts_as_address on a model.
18
+ module ClassMethods
19
+ attr_reader :acts_as_address_config
20
+ def acts_as_address(**options)
21
+ # Have to use yield_self(&not_null) intead of presence because NullColumn.present? => true.
22
+ not_null = ->(column) {
23
+ column.type.nil? ? nil : column
24
+ }
25
+ options = options.deep_symbolize_keys
26
+ default_config = {
27
+ state: {
28
+ #normalize: false,
29
+ #validate: false,
30
+
31
+ code_attribute: column_for_attribute(:state_code).yield_self(&not_null)&.name ||
32
+ (column_for_attribute(:state).yield_self(&not_null)&.name unless options.dig(:state, :name_attribute).to_s == 'state'),
33
+
34
+ name_attribute: column_for_attribute(:state_name).yield_self(&not_null)&.name ||
35
+ (column_for_attribute(:state).yield_self(&not_null)&.name unless options.dig(:state, :code_attribute).to_s == 'state'),
36
+
37
+ on_unknown: ->(value, name_or_code) { },
38
+ },
39
+
40
+ country: {
41
+ #normalize: false,
42
+ #validate: false,
43
+
44
+ # By default, code (same as alpha_2_code) will be used
45
+ carmen_code: :code, # or alpha_2_code, alpha_3_code, :numeric_code
46
+
47
+ code_attribute: column_for_attribute(:country_code).yield_self(&not_null)&.name ||
48
+ (column_for_attribute(:country).yield_self(&not_null)&.name unless options.dig(:country, :name_attribute).to_s == 'country'),
49
+
50
+ name_attribute: column_for_attribute(:country_name).yield_self(&not_null)&.name ||
51
+ (column_for_attribute(:country).yield_self(&not_null)&.name unless options.dig(:country, :code_attribute).to_s == 'country'),
52
+
53
+ on_unknown: ->(value, name_or_code) { },
54
+ },
55
+
56
+ address: {
57
+ #normalize: false,
58
+ #validate: false,
59
+
60
+ # Try to auto-detect address columns
61
+ attributes: column_names.grep(/address$|^address_\d$/),
62
+ }
63
+ }
64
+ @acts_as_address_config = config = {
65
+ **default_config
66
+ }.deep_merge(options)
67
+
68
+ [:state, :country].each do |group|
69
+ # Can't use the same column for code and name, so if it would be the same (by default or
70
+ # otherwise), let it be used for name only instead.
71
+ if config[group][:code_attribute] == config[group][:name_attribute]
72
+ config[group].delete(:code_attribute)
73
+ end
74
+ end
75
+
76
+ include ::AddressConcern::Address
77
+ end
78
+
79
+ def belongs_to_addressable(**options)
80
+ belongs_to :addressable, polymorphic: true, touch: true, optional: true, **options
81
+ end
82
+ end
83
+ end
84
+
85
+ include InspectBase
86
+ include AttributesSlice
87
+
88
+ extend ActiveSupport::Concern
89
+ included do
90
+ #═══════════════════════════════════════════════════════════════════════════════════════════════
91
+ # Config
92
+
93
+ delegate *[
94
+ :acts_as_address_config,
95
+ :country_config,
96
+ :state_config,
97
+ ], to: 'self.class'
98
+
99
+ class << self
100
+ #─────────────────────────────────────────────────────────────────────────────────────────────
101
+ def country_config
102
+ @acts_as_address_config[:country] || {}
103
+ end
104
+
105
+ # usually :code
106
+ def carmen_country_code
107
+ country_config[:carmen_code]
108
+ end
109
+
110
+ # usually :coded
111
+ def carmen_country_code_find_method
112
+ :"#{carmen_country_code}d"
113
+ end
114
+
115
+ # 'country' or similar
116
+ def country_name_attribute
117
+ country_config[:name_attribute]&.to_sym
118
+ end
119
+
120
+ def country_code_attribute
121
+ country_config[:code_attribute]&.to_sym
122
+ end
123
+
124
+ #─────────────────────────────────────────────────────────────────────────────────────────────
125
+
126
+ def state_config
127
+ @acts_as_address_config[:state] || {}
128
+ end
129
+
130
+ def carmen_state_code
131
+ state_config[:carmen_code]
132
+ end
133
+
134
+ def state_name_attribute
135
+ state_config[:name_attribute]&.to_sym
136
+ end
137
+
138
+ def state_code_attribute
139
+ state_config[:code_attribute]&.to_sym
140
+ end
141
+
142
+ #─────────────────────────────────────────────────────────────────────────────────────────────
143
+
144
+ def address_attr_config
145
+ @acts_as_address_config[:address] || {}
146
+ end
147
+
148
+ # TODO: rename to something different than the same name as #address_attributes, like
149
+ # street_address_attr_names
150
+ def address_attributes
151
+ Array(address_attr_config[:attributes]).map(&:to_sym)
152
+ end
153
+
154
+ # Address line 1
155
+ def address_attribute
156
+ address_attributes[0]
157
+ end
158
+
159
+ def multi_line_address?
160
+ address_attributes.size == 1 && (
161
+ column = column_for_attribute(address_attribute)
162
+ column.type == :text
163
+ )
164
+ end
165
+
166
+ #─────────────────────────────────────────────────────────────────────────────────────────────
167
+
168
+ # AKA configured_address_attributes
169
+ def address_attr_names
170
+ [
171
+ *address_attributes,
172
+ :city,
173
+ state_name_attribute,
174
+ state_code_attribute,
175
+ :postal_code,
176
+ country_name_attribute,
177
+ country_code_attribute,
178
+ ].compact.uniq
179
+ end
180
+ end
181
+
182
+ #═════════════════════════════════════════════════════════════════════════════════════════════════
183
+ # Customizable validation (to add?)
184
+
185
+ #validates_presence_of :address
186
+ #validates_presence_of :state, if: :state_required?
187
+ #validates_presence_of :country
188
+
189
+ #═════════════════════════════════════════════════════════════════════════════════════════════════
190
+ # Attributes
191
+
192
+ def _assign_attributes(attributes)
193
+ attributes = attributes.symbolize_keys
194
+ attributes = reorder_language_attributes(attributes)
195
+ super(attributes)
196
+ end
197
+
198
+ def self.country_aliases ; [:country_name, :country_code] ; end
199
+ def self.state_aliases ; [:state_name, :state_code] ; end
200
+
201
+ # country needs to be assigned _before_ state for things to work as intended (can't look up
202
+ # state in state= unless we know which country it is for)
203
+ def reorder_language_attributes(attributes)
204
+ attributes.reorder(self.class.country_name_attribute, self.class.country_code_attribute, *self.class.country_aliases,
205
+ self.class. state_name_attribute, self.class. state_code_attribute, *self.class.state_aliases)
206
+ end
207
+
208
+ def address_attributes
209
+ attributes_slice(
210
+ *self.class.address_attr_names
211
+ )
212
+ end
213
+
214
+ #═════════════════════════════════════════════════════════════════════════════════════════════════
215
+
216
+ # TODO: automatically normalize if attribute_normalizer/normalizy gem is loaded? add a config option to opt out?
217
+ #normalize_attributes :city, :state, :postal_code, :country
218
+ #normalize_attribute *address_attributes, with: [:cleanlines, :strip]
219
+
220
+ #═════════════════════════════════════════════════════════════════════════════════════════════════
221
+ # Country & State (Carmen + custom)
222
+
223
+ # Some of these methods look up by either name or code
224
+
225
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
226
+ # find country
227
+
228
+ # Finds by name, falling back to finding by code.
229
+ def self.find_carmen_country(name)
230
+ return name if name.is_a? Carmen::Country
231
+
232
+ (
233
+ find_carmen_country_by_name(name) ||
234
+ find_carmen_country_by_code(name)
235
+ )
236
+ end
237
+ def self.find_carmen_country!(name)
238
+ find_carmen_country(name) or
239
+ raise "country #{name} not found"
240
+ end
241
+
242
+ def self.find_carmen_country_by_name(name)
243
+ name = recognize_country_name_alias(name)
244
+ Carmen::Country.named(name)
245
+ end
246
+
247
+ def self.find_carmen_country_by_code(code)
248
+ # Carmen::Country.coded(code)
249
+ Carmen::Country.send(carmen_country_code_find_method, code)
250
+ end
251
+
252
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
253
+ # find state
254
+
255
+ # Finds by name, falling back to finding by code.
256
+ def self.find_carmen_state(country_name, name)
257
+ return name if name.is_a? Carmen::Region
258
+
259
+ country = find_carmen_country!(country_name)
260
+ states = states_for_country(country)
261
+ (
262
+ states.named(name) ||
263
+ states.coded(name)
264
+ )
265
+ end
266
+ def self.find_carmen_state!(country_name, name)
267
+ find_carmen_state(country_name, name) or
268
+ raise "state #{name} not found for country #{country_name}"
269
+ end
270
+
271
+ def self.find_carmen_state_by_name(country_name, name)
272
+ country = find_carmen_country!(country_name)
273
+ states = states_for_country(country)
274
+ states.named(name)
275
+ end
276
+
277
+ def self.find_carmen_state_by_code(country_name, code)
278
+ country = find_carmen_country!(country_name)
279
+ states = states_for_country(country)
280
+ states.coded(code)
281
+ end
282
+
283
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
284
+ # country
285
+
286
+ # Calls country.code
287
+ _ = def self.carmen_country_code_for(country)
288
+ country.send(carmen_country_code)
289
+ end
290
+ delegate _, to: 'self.class'
291
+
292
+ # If you are storing both a country_name and country_code...
293
+ # This _should_ be the same as the value stored in the country attribute, but allows you to
294
+ # look it up just to make sure they match (or to update country field to match this).
295
+ def country_name_from_code
296
+ if (country = self.class.find_carmen_country_by_code(country_code))
297
+ country.name
298
+ end
299
+ end
300
+ def country_code_from_name
301
+ if (country = self.class.find_carmen_country_by_name(country_name))
302
+ self.class.carmen_country_code_for(country)
303
+ end
304
+ end
305
+
306
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
307
+ # state
308
+
309
+ def state_name_from_code
310
+ if carmen_country && (state = self.class.find_carmen_state_by_code(carmen_country, state_code))
311
+ state.name
312
+ end
313
+ end
314
+ def state_code_from_name
315
+ if carmen_country && (state = self.class.find_carmen_state_by_name(carmen_country, state_name))
316
+ state.code
317
+ end
318
+ end
319
+
320
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
321
+ # country
322
+
323
+ def self.recognize_country_name_alias(name)
324
+ name = case name
325
+ when 'USA'
326
+ 'United States'
327
+ when 'The Democratic Republic of the Congo', 'Democratic Republic of the Congo'
328
+ 'Congo, the Democratic Republic of the'
329
+ when 'Republic of Macedonia', 'Macedonia, Republic of', 'Macedonia'
330
+ 'Macedonia, Republic of'
331
+ else
332
+ name
333
+ end
334
+ end
335
+
336
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
337
+
338
+ scope :in_country, ->(country_name) {
339
+ country = find_carmen_country!(country_name)
340
+ where(addresses: { country_code: country&.code })
341
+ }
342
+ scope :in_state, ->(country_name, name) {
343
+ country = find_carmen_country!(country_name)
344
+ state = find_carmen_state!(country_name, name)
345
+ where(addresses: { country_code: country&.code, state_code: state&.code })
346
+ }
347
+
348
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
349
+
350
+ def carmen_country
351
+ self.class.find_carmen_country_by_code(country_code)
352
+ end
353
+
354
+ def carmen_state
355
+ if (country = carmen_country)
356
+ # country.subregions.coded(state_code)
357
+ self.class.states_for_country(country).coded(state_code)
358
+ end
359
+ end
360
+
361
+ #═════════════════════════════════════════════════════════════════════════════════════════════════
362
+ # country attribute(s)
363
+
364
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
365
+ # setters
366
+
367
+
368
+ def clear_country
369
+ write_attribute(self.class.country_name_attribute, nil) if self.class.country_name_attribute
370
+ write_attribute(self.class.country_code_attribute, nil) if self.class.country_code_attribute
371
+ end
372
+
373
+ def set_country_from_carmen_country(country)
374
+ write_attribute(self.class.country_name_attribute, country.name ) if self.class.country_name_attribute
375
+ write_attribute(self.class.country_code_attribute, carmen_country_code_for(country)) if self.class.country_code_attribute
376
+ end
377
+
378
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
379
+ # code=
380
+
381
+ # def country_code=(code)
382
+ define_method :"#{country_code_attribute || 'country_code'}=" do |value|
383
+ if value.blank?
384
+ clear_country
385
+ else
386
+ if (country = self.class.find_carmen_country_by_code(value))
387
+ set_country_from_carmen_country(country)
388
+ else
389
+ country_config[:on_unknown].(value, :code)
390
+ write_attribute(self.class.country_code_attribute, value) if self.class.country_code_attribute
391
+ end
392
+ end
393
+ end
394
+
395
+ # Attribute alias
396
+ if country_code_attribute
397
+ unless :country_code == country_code_attribute
398
+ alias_attribute :country_code, :"#{country_code_attribute}"
399
+ #alias_method :country_code=, :"#{country_code_attribute}="
400
+ end
401
+ else
402
+ alias_method :country_code, :country_code_from_name
403
+ end
404
+
405
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
406
+ # name=
407
+
408
+ # def country_name=(name)
409
+ define_method :"#{country_name_attribute || 'country_name'}=" do |value|
410
+ if value.blank?
411
+ clear_country
412
+ else
413
+ if (country = self.class.find_carmen_country_by_name(value))
414
+ set_country_from_carmen_country(country)
415
+ else
416
+ country_config[:on_unknown].(value, :name)
417
+ write_attribute(self.class.country_name_attribute, value) if self.class.country_name_attribute
418
+ end
419
+ end
420
+ end
421
+
422
+ # Attribute alias
423
+ if country_name_attribute
424
+ unless :country_name == country_name_attribute
425
+ alias_attribute :country_name, country_name_attribute
426
+ #alias_method :country_name=, :"#{country_name_attribute}="
427
+ end
428
+ else
429
+ alias_method :country_name, :country_name_from_code
430
+ end
431
+
432
+ #════════════════════════════════════════════════════════════════════════════════════════════════════
433
+ # state attribute(s)
434
+ # (This is nearly identical to country section above with s/country/state/)
435
+
436
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
437
+ # setters
438
+
439
+
440
+ def clear_state
441
+ write_attribute(self.class.state_name_attribute, nil) if self.class.state_name_attribute
442
+ write_attribute(self.class.state_code_attribute, nil) if self.class.state_code_attribute
443
+ end
444
+
445
+ def set_state_from_carmen_state(state)
446
+ write_attribute(self.class.state_name_attribute, state.name) if self.class.state_name_attribute
447
+ write_attribute(self.class.state_code_attribute, state.code) if self.class.state_code_attribute
448
+ end
449
+
450
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
451
+ # code=
452
+
453
+ # def state_code=(code)
454
+ define_method :"#{state_code_attribute || 'state_code'}=" do |value|
455
+ if value.blank?
456
+ clear_state
457
+ else
458
+ if carmen_country && (state = self.class.find_carmen_state_by_code(carmen_country, value))
459
+ set_state_from_carmen_state(state)
460
+ else
461
+ #puts carmen_country ? "unknown state code '#{value}'" : "can't find state without country"
462
+ state_config[:on_unknown].(value, :code)
463
+ write_attribute(self.class.state_code_attribute, value) if self.class.state_code_attribute
464
+ end
465
+ end
466
+ end
467
+
468
+ # Attribute alias
469
+ if state_code_attribute
470
+ unless :state_code == state_code_attribute
471
+ alias_attribute :state_code, :"#{state_code_attribute}"
472
+ #alias_method :state_code=, :"#{state_code_attribute}="
473
+ end
474
+ else
475
+ alias_method :state_code, :state_code_from_name
476
+ end
477
+
478
+ # alias_method :province, :state
479
+
480
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
481
+ # name=
482
+
483
+ # def state_name=(name)
484
+ # Uses find_carmen_state so if your column was named 'state', you could actually do state = name
485
+ # or code.
486
+ define_method :"#{state_name_attribute || 'state_name'}=" do |value|
487
+ if value.blank?
488
+ clear_state
489
+ else
490
+ if carmen_country && (state = self.class.find_carmen_state(carmen_country, value))
491
+ set_state_from_carmen_state(state)
492
+ else
493
+ #puts carmen_country ? "unknown state name '#{name}'" : "can't find state without country"
494
+ state_config[:on_unknown].(value, :name)
495
+ write_attribute(self.class.state_name_attribute, value) if self.class.state_name_attribute
496
+ end
497
+ end
498
+ end
499
+
500
+ # Attribute alias
501
+ if state_name_attribute
502
+ unless :state_name == state_name_attribute
503
+ alias_attribute :state_name, state_name_attribute
504
+ #alias_method :state_name=, :"#{state_name_attribute}="
505
+ end
506
+ else
507
+ alias_method :state_name, :state_name_from_code
508
+ end
509
+
510
+ #════════════════════════════════════════════════════════════════════════════════════════════════════
511
+ # State/province options for country
512
+
513
+ # This is useful if want to list the state options allowed for a country in a select box and
514
+ # restrict entry to only officially listed state options.
515
+ # It is not required in the postal address for all countries, however. If you only want to show it
516
+ # if it's required in the postal address, you can make it conditional based on
517
+ # state_included_in_postal_address?.
518
+ def self.states_for_country(country)
519
+ return [] unless country
520
+ country = find_carmen_country!(country)
521
+
522
+ has_states_at_level_1 = country.subregions.any? { |region|
523
+ region.type == 'state' ||
524
+ region.type == 'province' ||
525
+ region.type == 'metropolitan region'
526
+ }
527
+ has_states_at_level_1 = false if country.name == 'United Kingdom'
528
+
529
+ if country.name == 'Kenya'
530
+ # https://github.com/jim/carmen/issues/227
531
+ # https://en.wikipedia.org/wiki/Provinces_of_Kenya
532
+ # Kenya's provinces were replaced by a system of counties in 2013.
533
+ # https://en.wikipedia.org/wiki/ISO_3166-2:KE confirms that they are "former" provinces.
534
+ # At the time of this writing, however, it doesn't look like Carmen has been updated to
535
+ # include the 47 counties listed under https://en.wikipedia.org/wiki/ISO_3166-2:KE.
536
+ country.subregions.typed('county')
537
+ #elsif country.name == 'France'
538
+ # # https://github.com/jim/carmen/issues/228
539
+ # # https://en.wikipedia.org/wiki/Regions_of_France
540
+ # # In 2016 what had been 27 regions was reduced to 18.
541
+ # # France is divided into 18 administrative regions, including 13 metropolitan regions and 5 overseas regions.
542
+ # # https://en.wikipedia.org/wiki/ISO_3166-2:FR
543
+ # []
544
+ elsif has_states_at_level_1
545
+ country.subregions
546
+ else
547
+ # Going below level-1 subregions is needed for Philippines, Indonesia, and possibly others
548
+ Carmen::RegionCollection.new(
549
+ country.subregions.
550
+ map { |_| _.subregions.any? ? _.subregions : _ }.
551
+ flatten
552
+ )
553
+ end
554
+ end
555
+ def states_for_country
556
+ self.class.states_for_country(carmen_country)
557
+ end
558
+ alias_method :state_options, :states_for_country
559
+
560
+ def country_with_states?
561
+ states_for_country.any?
562
+ end
563
+
564
+ #───────────────────────────────────────────────────────────────────────────────────────────────
565
+
566
+ # Used for checking/testing states_for_country.
567
+ # Example:
568
+ # Address.compare_subregions_and_states_for_country('France');
569
+ def self.compare_subregions_and_states_for_country(country)
570
+ country = find_carmen_country!(country)
571
+ states_for_country = states_for_country(country)
572
+ if country.subregions == states_for_country
573
+ puts '(Same:)'
574
+ pp country.subregions
575
+ else
576
+ puts %(country.subregions (#{country.subregions.size}):\n#{country.subregions.pretty_inspect})
577
+ puts
578
+ puts %(states_for_country(country) (#{states_for_country.size}):\n#{states_for_country})
579
+ states_for_country
580
+ end
581
+ end
582
+
583
+ #───────────────────────────────────────────────────────────────────────────────────────────────
584
+
585
+ # Is the state/province required in a postal address?
586
+ # If no, perhaps you want to collect it for other reasons (like seeing which people/things are in
587
+ # the same region). Or for countries where it *may* be included in a postal address but is not
588
+ # required to be included.
589
+ def state_required_in_postal_address?
590
+ [
591
+ 'Australia',
592
+ 'Brazil',
593
+ 'Canada',
594
+ 'Mexico',
595
+ 'United States',
596
+ 'Italy',
597
+ 'Venezuela',
598
+ ].include? country_name
599
+ end
600
+ def state_possibly_included_in_postal_address?
601
+ # https://ux.stackexchange.com/questions/64665/address-form-field-for-region
602
+ # http://www.bitboost.com/ref/international-address-formats/denmark/
603
+ # http://www.bitboost.com/ref/international-address-formats/poland/
604
+ return true if state_required_in_postal_address?
605
+ return false if [
606
+ 'Algeria',
607
+ 'Argentina',
608
+ 'Austria',
609
+ 'Denmark',
610
+ 'France',
611
+ 'Germany',
612
+ 'Indonesia',
613
+ 'Ireland',
614
+ 'Israel',
615
+ 'Netherlands',
616
+ 'New Zealand',
617
+ 'Poland',
618
+ 'Sweden',
619
+ 'United Kingdom',
620
+ ].include? country_name
621
+ # Default:
622
+ country_with_states?
623
+ end
624
+
625
+ # It's not called a "State" in all countries.
626
+ # In some countries, it could technically be multiple different types of regions:
627
+ # - In United States, it could be a state or an outlying region or a district or an APO
628
+ # - In Canada, it could be a province or a territory.
629
+ # This attempts to return the most common, expected name for this field.
630
+ # See also: https://ux.stackexchange.com/questions/64665/address-form-field-for-region
631
+ #
632
+ # To see what it should be called in all countries known to Carmen:
633
+ # Country.countries_with_states.map {|country| [country.name, Address.new(country_name: country.name).state_label] }.to_h
634
+ # => {"Afghanistan"=>"Province",
635
+ # "Armenia"=>"Province",
636
+ # "Angola"=>"Province",
637
+ # "Argentina"=>"Province",
638
+ # "Austria"=>"State",
639
+ # "Australia"=>"State",
640
+ # ...
641
+ def state_label
642
+ # In UK, it looks like they (optionally) include the *county* in their addresses. They don't actually have "states" per se.
643
+ # Reference: http://bitboost.com/ref/international-address-formats/united-kingdom/
644
+ # Could also limit to Countries (England, Scotland, Wales) and Provinces (Northern Ireland).
645
+ # Who knows. The UK's subregions are a mess.
646
+ # If allowing the full list of subregions from https://en.wikipedia.org/wiki/ISO_3166-2:GB,
647
+ # perhaps Region is a better, more inclusive term.
648
+ if country_name.in? ['United Kingdom']
649
+ 'Region'
650
+ elsif state_options.any?
651
+ state_options[0].type.capitalize
652
+ end
653
+ end
654
+
655
+ #════════════════════════════════════════════════════════════════════════════════════════════════════
656
+
657
+ def empty?
658
+ address_attributes.all? do |key, value|
659
+ value.blank?
660
+ end
661
+ end
662
+
663
+ def present?
664
+ address_attributes.any? do |key, value|
665
+ value.present?
666
+ end
667
+ end
668
+
669
+ #════════════════════════════════════════════════════════════════════════════════════════════════════
670
+ # Street address / Address lines
671
+
672
+ # Attribute alias for street address line 1
673
+ #if address_attribute
674
+ # unless :address == address_attribute
675
+ # alias_attribute :address, :"#{address_attribute}"
676
+ # end
677
+ #end
678
+
679
+ def address_lines
680
+ if self.class.multi_line_address?
681
+ address.to_s.cleanlines.to_a
682
+ else
683
+ self.class.address_attributes.map do |attr_name|
684
+ send attr_name
685
+ end
686
+ end
687
+ end
688
+
689
+ #════════════════════════════════════════════════════════════════════════════════════════════════════
690
+ # Formatting for humans
691
+
692
+ # Lines of a postal address
693
+ def lines
694
+ [
695
+ #name,
696
+ *address_lines,
697
+ city_line,
698
+ country_name,
699
+ ].reject(&:blank?)
700
+ end
701
+
702
+ # Used by #lines
703
+ #
704
+ # Instead of using `state` method (which is really state_code). That's fine for some countries
705
+ # like US, Canada, Australia but not other countries (presumably).
706
+ #
707
+ # TODO: Put postal code and city in a different order, as that country's conventions dictate.
708
+ # See http://bitboost.com/ref/international-address-formats/new-zealand/
709
+ #
710
+ def city_line
711
+ [
712
+ #[city, state].reject(&:blank?).join(', '),
713
+ [city, state_for_postal_address].reject(&:blank?).join(', '),
714
+ postal_code,
715
+ ].reject(&:blank?).join(' ')
716
+ end
717
+
718
+ def city_state_code
719
+ [city, state_code].reject(&:blank?).join(', ')
720
+ end
721
+
722
+ def city_state_name
723
+ [city, state_name].reject(&:blank?).join(', ')
724
+ end
725
+
726
+ def city_state_country
727
+ [city_state_name, country_name].join(', ')
728
+ end
729
+
730
+ def state_for_postal_address
731
+ # Possibly others use a code? But seems safer to default to a name until confirmed that they use
732
+ # a code.
733
+ if country_name.in? ['United States', 'Canada', 'Australia']
734
+ state_code
735
+ elsif state_possibly_included_in_postal_address?
736
+ state_name
737
+ else
738
+ ''
739
+ end
740
+ end
741
+
742
+ #════════════════════════════════════════════════════════════════════════════════════════════════════
743
+ # Misc. output
744
+
745
+ # TODO: remove?
746
+ def parts
747
+ [
748
+ #name,
749
+ *address_lines,
750
+ city,
751
+ state_name,
752
+ postal_code,
753
+ country_name,
754
+ ].reject(&:blank?)
755
+ end
756
+
757
+ # def inspect
758
+ # inspect_base(
759
+ # :id,
760
+ # #:name,
761
+ # :address,
762
+ # # address_2 ...
763
+ # :city,
764
+ # :state,
765
+ # :postal_code,
766
+ # :country,
767
+ # )
768
+ # end
769
+
770
+ def inspect
771
+ inspect_base(
772
+ :id,
773
+ address_attributes
774
+ )
775
+ end
776
+
777
+ #─────────────────────────────────────────────────────────────────────────────────────────────────
778
+ end
779
+ end
780
+ end
781
+
782
+ ActiveRecord::Base.class_eval do
783
+ include AddressConcern::Address::Base
784
+ end