address_concern 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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