decoupage_administratif 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15d8baade56f4081ec11036e1a10269d9a561eaca357835a9d6f81ae798749ec
4
- data.tar.gz: dc037fd227031b91ed1f9c44035ac1ea5c58b0322ca40f7f81cff54b9327017a
3
+ metadata.gz: 849db1d260bc7687d74158eb642a2a49c63dffc39bf20ff53635231b9875d955
4
+ data.tar.gz: cae4db6bbef56db6524be7d77fd6a10a78656d17c98c07ed4bd1d4f823e0b2d4
5
5
  SHA512:
6
- metadata.gz: 6203a3495a45bbcde0201432929485e5d6aa5b8270a8afed54936712561b8c890dbea27c6a9763781e7fa1a40a8a0fef6863dbd368371313d024a4e0584850f1
7
- data.tar.gz: b549a507147fba9e578d1104d521c4f2bea2e3a9d1cf9ab97406d81073b7c2fdf5f1428fa8acc23fa18ad5784bd17099c4a0218f6e39000aed503afe85d05073
6
+ metadata.gz: ddc815e22292f9318259046937c23fc7754ba1565d47ac9c49ea24c57bab315a858d4c2df782e04b7bb92583bd011ee64d770b0f11084f6f8e02a2779e7d4ac6
7
+ data.tar.gz: 8933516d15633dc0f4a60df35ae2039a28328c2e0ca29eda49867810b1edeebf2c177917c2d94b011cc134811c6c6799934062f37d226e9f9c17f03b870e7208
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2025-11-14
4
+
5
+ ### Added
6
+ - **BaseModel**: Support for array values in `where` method to filter on multiple values (e.g., `where(commune_type: [:commune_actuelle, :arrondissement_municipal])`)
7
+ - **Commune**: Municipal districts (arrondissements of Paris, Lyon, Marseille) are now included in `Commune.actuelles` method
8
+ - New commune type `:arrondissement_municipal` for municipal districts
9
+
10
+ ### Changed
11
+ - `Commune.actuelles` now returns both current communes and municipal districts (`:commune_actuelle` and `:arrondissement_municipal`)
12
+
13
+ ### Fixed
14
+ - RBS type signature for `commune_type` attribute corrected from `String` to `Symbol`
15
+
3
16
  ## [0.2.0] - 2025-08-29
4
17
 
5
18
  ### Changed
data/README.md CHANGED
@@ -63,10 +63,13 @@ Exemple d’utilisation basique :
63
63
  DecoupageAdministratif::Region.all
64
64
 
65
65
  # Trouver une commune par code INSEE
66
- # Une commune a un commune_type qui peut être `commune-actuelle`, `commune-deleguee` (anciennes communes) ou `commune-associee` (anciennes communes avec un statut spécial)
66
+ # Une commune a un commune_type qui peut être :commune_actuelle, :commune_deleguee (anciennes communes), :commune_associee (anciennes communes avec un statut spécial) ou :arrondissement_municipal (arrondissements de Paris, Lyon, Marseille)
67
67
  commune = DecoupageAdministratif::Commune.find('75056')
68
68
  puts commune.nom # => "Paris"
69
69
 
70
+ # Lister toutes les communes actuelles (communes actuelles + arrondissements municipaux)
71
+ DecoupageAdministratif::Commune.actuelles
72
+
70
73
  # Lister les départements d'une région
71
74
  DecoupageAdministratif::Region.find('84').departements
72
75
 
@@ -224,10 +227,13 @@ Basic usage example:
224
227
  DecoupageAdministratif::Region.all
225
228
 
226
229
  # Find a municipality by INSEE code
227
- # A municipality has a `commune_type` which can be :commune_actuelle (current municipalities), :commune_deleguee (former municipalities), or :commune_associee (former municipalities with a special status)
230
+ # A municipality has a `commune_type` which can be :commune_actuelle (current municipalities), :commune_deleguee (former municipalities), :commune_associee (former municipalities with a special status), or :arrondissement_municipal (municipal districts of Paris, Lyon, Marseille)
228
231
  commune = DecoupageAdministratif::Commune.find('75056')
229
232
  puts commune.nom # => "Paris"
230
233
 
234
+ # List all current municipalities (current communes + municipal districts)
235
+ DecoupageAdministratif::Commune.actuelles
236
+
231
237
  # List departments of a region
232
238
  DecoupageAdministratif::Region.find('84').departements
233
239
 
@@ -24,37 +24,103 @@ module DecoupageAdministratif
24
24
  all.find { |item| criteria.all? { |k, v| item.send(k) == v } }
25
25
  end
26
26
 
27
+ # Filter records based on criteria with optional case-insensitive and partial matching
28
+ #
27
29
  # @param args [Hash] keyword arguments containing filter criteria and options
28
- # @option args [Boolean] :case_insensitive perform case-insensitive matching
29
- # @option args [Boolean] :partial allow partial string matching (contains)
30
+ # @option args [Boolean] :case_insensitive perform case-insensitive matching (only for String values)
31
+ # @option args [Boolean] :partial allow partial string matching using contains logic (only for String values)
32
+ #
33
+ # @note When filtering with an Array:
34
+ # - Without options: checks if the item's value is included in the array (similar to SQL IN clause)
35
+ # - With case_insensitive: performs case-insensitive matching for each array element (String values only)
36
+ # - With partial: checks if item's value contains any of the array elements (String values only)
37
+ # - Both options can be combined for case-insensitive partial matching
38
+ #
30
39
  # @return [Array] an array of all items that match the criteria
31
- # @example
40
+ #
41
+ # @example Simple filtering
32
42
  # DecoupageAdministratif::Departement.where(code_region: '52')
43
+ #
44
+ # @example Case-insensitive and partial matching
33
45
  # DecoupageAdministratif::Commune.where(nom: 'pari', case_insensitive: true, partial: true)
34
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
46
+ #
47
+ # @example Array filtering (SQL IN-like behavior)
48
+ # DecoupageAdministratif::Commune.where(commune_type: [:commune_actuelle, :arrondissement_municipal])
49
+ #
50
+ # @example Array filtering with case-insensitive matching
51
+ # DecoupageAdministratif::Commune.where(nom: ['paris', 'mamers'], case_insensitive: true)
52
+ #
53
+ # @example Array filtering with partial matching
54
+ # DecoupageAdministratif::Commune.where(nom: ['par', 'mam'], partial: true)
35
55
  def where(**args)
36
- # Separate options from criteria
37
56
  case_insensitive = args.delete(:case_insensitive) || false
38
57
  partial = args.delete(:partial) || false
39
58
 
40
59
  all.select do |item|
41
- args.all? do |key, value|
42
- item_value = item.send(key)
43
-
44
- if case_insensitive && value.is_a?(String) && item_value.is_a?(String)
45
- if partial
46
- item_value.downcase.include?(value.downcase)
47
- else
48
- item_value.downcase == value.downcase
49
- end
50
- elsif partial && value.is_a?(String) && item_value.is_a?(String)
51
- item_value.include?(value)
52
- else
53
- item_value == value
54
- end
55
- end
60
+ args.all? { |key, value| matches?(item.send(key), value, case_insensitive, partial) }
56
61
  end
57
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
62
+ end
63
+
64
+ private
65
+
66
+ # Check if item_value matches the filter_value based on options
67
+ #
68
+ # @param item_value [Object] the value from the item being filtered
69
+ # @param filter_value [Object, Array] the value(s) to filter by
70
+ # @param case_insensitive [Boolean] whether to perform case-insensitive matching
71
+ # @param partial [Boolean] whether to perform partial (contains) matching
72
+ # @return [Boolean] true if the value matches the filter
73
+ def matches?(item_value, filter_value, case_insensitive, partial)
74
+ return match_array?(item_value, filter_value, case_insensitive, partial) if filter_value.is_a?(Array)
75
+ return match_single_value?(item_value, filter_value, case_insensitive, partial) if strings?(item_value, filter_value)
76
+
77
+ item_value == filter_value
78
+ end
79
+
80
+ # Check if item_value matches any value in the filter array
81
+ #
82
+ # @param item_value [Object] the value from the item being filtered
83
+ # @param filter_array [Array] array of values to match against
84
+ # @param case_insensitive [Boolean] whether to perform case-insensitive matching
85
+ # @param partial [Boolean] whether to perform partial (contains) matching
86
+ # @return [Boolean] true if the value matches any element in the array
87
+ def match_array?(item_value, filter_array, case_insensitive, partial)
88
+ return filter_array.include?(item_value) unless apply_string_options?(item_value, filter_array)
89
+
90
+ filter_array.any? { |filter_element| match_single_value?(item_value, filter_element, case_insensitive, partial) }
91
+ end
92
+
93
+ # Check if item_value matches filter_value with string options
94
+ #
95
+ # @param item_value [String] the string value from the item
96
+ # @param filter_value [String] the string value to filter by
97
+ # @param case_insensitive [Boolean] whether to perform case-insensitive matching
98
+ # @param partial [Boolean] whether to use partial (contains) matching
99
+ # @return [Boolean] true if strings match according to options
100
+ def match_single_value?(item_value, filter_value, case_insensitive, partial)
101
+ return item_value.downcase.include?(filter_value.downcase) if case_insensitive && partial
102
+ return item_value.downcase == filter_value.downcase if case_insensitive
103
+ return item_value.include?(filter_value) if partial
104
+
105
+ item_value == filter_value
106
+ end
107
+
108
+ # Check if both values are strings
109
+ #
110
+ # @param value1 [Object] first value to check
111
+ # @param value2 [Object] second value to check
112
+ # @return [Boolean] true if both values are strings
113
+ def strings?(value1, value2)
114
+ value1.is_a?(String) && value2.is_a?(String)
115
+ end
116
+
117
+ # Check if string matching options should be applied
118
+ #
119
+ # @param item_value [Object] the value from the item
120
+ # @param filter_array [Array] the filter array
121
+ # @return [Boolean] true if string options should be applied
122
+ def apply_string_options?(item_value, filter_array)
123
+ item_value.is_a?(String) && filter_array.all? { |v| v.is_a?(String) }
58
124
  end
59
125
  end
60
126
  end
@@ -20,6 +20,7 @@ module DecoupageAdministratif
20
20
  # - :commune_actuelle: Standard current commune
21
21
  # - :commune_deleguee: Delegated commune
22
22
  # - :commune_associee: Associated commune
23
+ # - :arrondissement_municipal: Municipal district (Paris, Lyon, Marseille)
23
24
  # @note Default value is :commune_actuelle
24
25
  attr_reader :code, :nom, :zone, :region_code, :departement_code, :commune_type
25
26
 
@@ -54,9 +55,9 @@ module DecoupageAdministratif
54
55
  end
55
56
  end
56
57
 
57
- # @return [Array<Commune>] a collection of all communes _actuelles_
58
+ # @return [Array<Commune>] a collection of all communes _actuelles_ and municipal districts
58
59
  def self.actuelles
59
- @actuelles ||= where(commune_type: :commune_actuelle)
60
+ @actuelles ||= where(commune_type: %i[commune_actuelle arrondissement_municipal])
60
61
  end
61
62
 
62
63
  # @raise [NotFoundError] if no region is found for the code
@@ -4,6 +4,7 @@ module DecoupageAdministratif
4
4
  class Departement
5
5
  extend BaseModel
6
6
  include TerritoryExtensions
7
+
7
8
  # @!attribute [r] code
8
9
  # @return [String] INSEE code of the department
9
10
  # @!attribute [r] nom
@@ -118,7 +118,7 @@ module DecoupageAdministratif
118
118
  # Check if only one key and it is nil
119
119
  # @return [Boolean]
120
120
  def only_nil_key?
121
- @codes.keys.uniq.count == 1 && @codes.keys.first.nil?
121
+ @codes.keys.uniq.one? && @codes.keys.first.nil?
122
122
  end
123
123
 
124
124
  # Should add departement to result?
@@ -154,7 +154,7 @@ module DecoupageAdministratif
154
154
  # @return [void]
155
155
  def search_for_epcis
156
156
  @epcis = []
157
- return if @codes.keys.uniq.count == 1 && @codes.keys.first.nil?
157
+ return if @codes.keys.uniq.one? && @codes.keys.first.nil?
158
158
 
159
159
  @codes.each_value do |communes|
160
160
  # Find EPCIs that match all communes in the current group
@@ -171,7 +171,7 @@ module DecoupageAdministratif
171
171
  # @return [void]
172
172
  def search_for_communes
173
173
  @communes = []
174
- return if @codes.keys.uniq.count == 1 && @codes.keys.first.nil?
174
+ return if @codes.keys.uniq.one? && @codes.keys.first.nil?
175
175
 
176
176
  @codes.each_value do |communes|
177
177
  communes.map { |commune| @communes << commune }
@@ -10,7 +10,7 @@ module DecoupageAdministratif
10
10
  def includes_any_commune_code?(commune_insee_codes)
11
11
  return false if commune_insee_codes.empty?
12
12
 
13
- check_inclusion(commune_insee_codes)
13
+ check_inclusion?(commune_insee_codes)
14
14
  end
15
15
 
16
16
  def insee_codes
@@ -21,7 +21,7 @@ module DecoupageAdministratif
21
21
 
22
22
  attr_reader :territory
23
23
 
24
- def check_inclusion(commune_insee_codes)
24
+ def check_inclusion?(commune_insee_codes)
25
25
  raise NotImplementedError, "Must be implemented by subclass"
26
26
  end
27
27
 
@@ -33,7 +33,7 @@ module DecoupageAdministratif
33
33
  class CommuneStrategy < Base
34
34
  private
35
35
 
36
- def check_inclusion(commune_insee_codes)
36
+ def check_inclusion?(commune_insee_codes)
37
37
  commune_insee_codes.include?(territory.code)
38
38
  end
39
39
 
@@ -45,7 +45,7 @@ module DecoupageAdministratif
45
45
  class DepartementStrategy < Base
46
46
  private
47
47
 
48
- def check_inclusion(commune_insee_codes)
48
+ def check_inclusion?(commune_insee_codes)
49
49
  departement_prefix = territory.code.length == 2 ? territory.code : territory.code[0..1]
50
50
  commune_insee_codes.any? { |commune_code| commune_code.start_with?(departement_prefix) }
51
51
  end
@@ -58,7 +58,7 @@ module DecoupageAdministratif
58
58
  class RegionStrategy < Base
59
59
  private
60
60
 
61
- def check_inclusion(commune_insee_codes)
61
+ def check_inclusion?(commune_insee_codes)
62
62
  departement_codes = territory.departements.map(&:code)
63
63
  commune_insee_codes.any? do |commune_code|
64
64
  dept_code = commune_code.length >= 3 && commune_code[0..1].to_i >= 96 ? commune_code[0..2] : commune_code[0..1]
@@ -74,7 +74,7 @@ module DecoupageAdministratif
74
74
  class EpciStrategy < Base
75
75
  private
76
76
 
77
- def check_inclusion(commune_insee_codes)
77
+ def check_inclusion?(commune_insee_codes)
78
78
  epci_commune_codes = territory.membres.map { |membre| membre[:code] || membre["code"] }
79
79
  (epci_commune_codes & commune_insee_codes).any?
80
80
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DecoupageAdministratif
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  DATA_VERSION = "5.2.0"
6
6
  DATA_SOURCE = "@etalab/decoupage-administratif"
7
7
  end
@@ -2,6 +2,14 @@ module DecoupageAdministratif
2
2
  module BaseModel
3
3
  def find: (String) -> untyped
4
4
  def find_by: (Hash[Symbol, untyped]) -> untyped
5
- def where: (Hash[Symbol, untyped]) -> Array[untyped]
5
+ def where: (**untyped) -> Array[untyped]
6
+
7
+ private
8
+
9
+ def matches?: (untyped item_value, untyped filter_value, bool case_insensitive, bool partial) -> bool
10
+ def match_array?: (untyped item_value, Array[untyped] filter_array, bool case_insensitive, bool partial) -> bool
11
+ def match_single_value?: (String item_value, String filter_value, bool case_insensitive, bool partial) -> bool
12
+ def strings?: (untyped value1, untyped value2) -> bool
13
+ def apply_string_options?: (untyped item_value, Array[untyped] filter_array) -> bool
6
14
  end
7
15
  end
@@ -8,7 +8,7 @@ module DecoupageAdministratif
8
8
  attr_reader zone: String
9
9
  attr_reader region_code: String
10
10
  attr_reader departement_code: String
11
- attr_reader commune_type: String
11
+ attr_reader commune_type: Symbol
12
12
 
13
13
  def initialize: (
14
14
  code: String,
@@ -21,7 +21,7 @@ module DecoupageAdministratif
21
21
 
22
22
  def self.all: () -> Array[Commune]
23
23
 
24
- def self.communes_actuelles: () -> Array[Commune]
24
+ def self.actuelles: () -> Array[Commune]
25
25
 
26
26
  def region: () -> Region
27
27
 
@@ -11,7 +11,7 @@ module DecoupageAdministratif
11
11
 
12
12
  attr_reader territory: untyped
13
13
 
14
- def check_inclusion: (Array[String] commune_insee_codes) -> bool
14
+ def check_inclusion?: (Array[String] commune_insee_codes) -> bool
15
15
 
16
16
  def calculate_insee_codes: () -> Array[String]
17
17
  end
@@ -19,7 +19,7 @@ module DecoupageAdministratif
19
19
  class CommuneStrategy < Base
20
20
  private
21
21
 
22
- def check_inclusion: (Array[String] commune_insee_codes) -> bool
22
+ def check_inclusion?: (Array[String] commune_insee_codes) -> bool
23
23
 
24
24
  def calculate_insee_codes: () -> Array[String]
25
25
  end
@@ -27,7 +27,7 @@ module DecoupageAdministratif
27
27
  class DepartementStrategy < Base
28
28
  private
29
29
 
30
- def check_inclusion: (Array[String] commune_insee_codes) -> bool
30
+ def check_inclusion?: (Array[String] commune_insee_codes) -> bool
31
31
 
32
32
  def calculate_insee_codes: () -> Array[String]
33
33
  end
@@ -35,7 +35,7 @@ module DecoupageAdministratif
35
35
  class RegionStrategy < Base
36
36
  private
37
37
 
38
- def check_inclusion: (Array[String] commune_insee_codes) -> bool
38
+ def check_inclusion?: (Array[String] commune_insee_codes) -> bool
39
39
 
40
40
  def calculate_insee_codes: () -> Array[String]
41
41
  end
@@ -43,7 +43,7 @@ module DecoupageAdministratif
43
43
  class EpciStrategy < Base
44
44
  private
45
45
 
46
- def check_inclusion: (Array[String] commune_insee_codes) -> bool
46
+ def check_inclusion?: (Array[String] commune_insee_codes) -> bool
47
47
 
48
48
  def calculate_insee_codes: () -> Array[String]
49
49
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decoupage_administratif
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucien Mollard
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-29 00:00:00.000000000 Z
11
+ date: 2025-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake