decoupage_administratif 0.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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +55 -0
  4. data/CHANGELOG.md +35 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +320 -0
  7. data/Rakefile +14 -0
  8. data/data/communes.json +37640 -0
  9. data/data/departements.json +111 -0
  10. data/data/epci.json +1257 -0
  11. data/data/regions.json +28 -0
  12. data/lib/decoupage_administratif/base_model.rb +60 -0
  13. data/lib/decoupage_administratif/commune.rb +82 -0
  14. data/lib/decoupage_administratif/config.rb +47 -0
  15. data/lib/decoupage_administratif/departement.rb +51 -0
  16. data/lib/decoupage_administratif/epci.rb +61 -0
  17. data/lib/decoupage_administratif/parser.rb +41 -0
  18. data/lib/decoupage_administratif/railtie.rb +11 -0
  19. data/lib/decoupage_administratif/region.rb +50 -0
  20. data/lib/decoupage_administratif/search.rb +181 -0
  21. data/lib/decoupage_administratif/territory_extensions.rb +35 -0
  22. data/lib/decoupage_administratif/territory_strategies.rb +87 -0
  23. data/lib/decoupage_administratif/version.rb +7 -0
  24. data/lib/decoupage_administratif.rb +26 -0
  25. data/lib/tasks/install.rake +59 -0
  26. data/sig/decoupage_administratif/base_model.rbs +7 -0
  27. data/sig/decoupage_administratif/commune.rbs +32 -0
  28. data/sig/decoupage_administratif/departement.rbs +24 -0
  29. data/sig/decoupage_administratif/epci.rbs +23 -0
  30. data/sig/decoupage_administratif/parser.rbs +11 -0
  31. data/sig/decoupage_administratif/region.rbs +21 -0
  32. data/sig/decoupage_administratif/search.rbs +20 -0
  33. data/sig/decoupage_administratif/territory_extensions.rbs +11 -0
  34. data/sig/decoupage_administratif/territory_strategies.rbs +51 -0
  35. data/sig/decoupage_administratif.rbs +4 -0
  36. metadata +96 -0
data/data/regions.json ADDED
@@ -0,0 +1,28 @@
1
+ [
2
+ {"code":"01","chefLieu":"97105","nom":"Guadeloupe","typeLiaison":3,"zone":"drom"},
3
+ {"code":"02","chefLieu":"97209","nom":"Martinique","typeLiaison":3,"zone":"drom"},
4
+ {"code":"03","chefLieu":"97302","nom":"Guyane","typeLiaison":3,"zone":"drom"},
5
+ {"code":"04","chefLieu":"97411","nom":"La Réunion","typeLiaison":0,"zone":"drom"},
6
+ {"code":"06","chefLieu":"97611","nom":"Mayotte","typeLiaison":0,"zone":"drom"},
7
+ {"code":"11","chefLieu":"75056","nom":"Île-de-France","typeLiaison":1,"zone":"metro"},
8
+ {"code":"24","chefLieu":"45234","nom":"Centre-Val de Loire","typeLiaison":2,"zone":"metro"},
9
+ {"code":"27","chefLieu":"21231","nom":"Bourgogne-Franche-Comté","typeLiaison":0,"zone":"metro"},
10
+ {"code":"28","chefLieu":"76540","nom":"Normandie","typeLiaison":0,"zone":"metro"},
11
+ {"code":"32","chefLieu":"59350","nom":"Hauts-de-France","typeLiaison":4,"zone":"metro"},
12
+ {"code":"44","chefLieu":"67482","nom":"Grand Est","typeLiaison":2,"zone":"metro"},
13
+ {"code":"52","chefLieu":"44109","nom":"Pays de la Loire","typeLiaison":4,"zone":"metro"},
14
+ {"code":"53","chefLieu":"35238","nom":"Bretagne","typeLiaison":0,"zone":"metro"},
15
+ {"code":"75","chefLieu":"33063","nom":"Nouvelle-Aquitaine","typeLiaison":3,"zone":"metro"},
16
+ {"code":"76","chefLieu":"31555","nom":"Occitanie","typeLiaison":1,"zone":"metro"},
17
+ {"code":"84","chefLieu":"69123","nom":"Auvergne-Rhône-Alpes","typeLiaison":1,"zone":"metro"},
18
+ {"code":"93","chefLieu":"13055","nom":"Provence-Alpes-Côte d'Azur","typeLiaison":0,"zone":"metro"},
19
+ {"code":"94","chefLieu":"2A004","nom":"Corse","typeLiaison":0,"zone":"metro"},
20
+ {"code":"975","chefLieu":"97502","nom":"Saint-Pierre-et-Miquelon","typeLiaison":0,"zone":"com"},
21
+ {"code":"977","chefLieu":"97701","nom":"Saint-Barthélemy","typeLiaison":0,"zone":"com"},
22
+ {"code":"978","chefLieu":"97801","nom":"Saint-Martin","typeLiaison":0,"zone":"com"},
23
+ {"code":"984","chefLieu":"97502","nom":"Terres australes et antarctiques françaises","typeLiaison":0,"zone":"com"},
24
+ {"code":"986","chefLieu":"98613","nom":"Wallis et Futuna","typeLiaison":0,"zone":"com"},
25
+ {"code":"987","chefLieu":"98735","nom":"Polynésie française","typeLiaison":0,"zone":"com"},
26
+ {"code":"988","chefLieu":"98818","nom":"Nouvelle-Calédonie","typeLiaison":0,"zone":"com"},
27
+ {"code":"989","chefLieu":"98901","nom":"Île de Clipperton","typeLiaison":0,"zone":"com"}
28
+ ]
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoupageAdministratif
4
+ module BaseModel
5
+ # Find a single record by its code (primary key equivalent)
6
+ # @param code [String] the code to search for
7
+ # @return [Object, nil] the element with the given code, or nil if not found
8
+ # @raise [NotFoundError] if the record is not found and no default block is given
9
+ # @example
10
+ # DecoupageAdministratif::Commune.find('72039')
11
+ # DecoupageAdministratif::Region.find('52')
12
+ def find(code)
13
+ result = find_by(code: code)
14
+ raise DecoupageAdministratif::NotFoundError, "#{name.split('::').last} not found for code #{code}" if result.nil?
15
+
16
+ result
17
+ end
18
+
19
+ # @param criteria [Hash] a hash with the attributes to filter by
20
+ # @return [untyped] the element that matches the criteria
21
+ # @example
22
+ # DecoupageAdministratif::Commune.find_by(nom: 'Paris')
23
+ def find_by(criteria)
24
+ all.find { |item| criteria.all? { |k, v| item.send(k) == v } }
25
+ end
26
+
27
+ # @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
+ # @return [Array] an array of all items that match the criteria
31
+ # @example
32
+ # DecoupageAdministratif::Departement.where(code_region: '52')
33
+ # DecoupageAdministratif::Commune.where(nom: 'pari', case_insensitive: true, partial: true)
34
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
35
+ def where(**args)
36
+ # Separate options from criteria
37
+ case_insensitive = args.delete(:case_insensitive) || false
38
+ partial = args.delete(:partial) || false
39
+
40
+ 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
56
+ end
57
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoupageAdministratif
4
+ class Commune
5
+ extend BaseModel
6
+ include TerritoryExtensions
7
+
8
+ # @!attribute [r] code
9
+ # @return [String] INSEE code of the commune
10
+ # @!attribute [r] nom
11
+ # @return [String] Name of the commune
12
+ # @!attribute [r] zone
13
+ # @return [String] Zone of the commune ("metro", "drom", "com")
14
+ # @!attribute [r] region_code
15
+ # @return [String] INSEE code of the region
16
+ # @!attribute [r] departement_code
17
+ # @return [String] INSEE code of the department
18
+ # @!attribute [r] commune_type
19
+ # @return [Symbol] Type of the commune. Possible values:
20
+ # - :commune_actuelle: Standard current commune
21
+ # - :commune_deleguee: Delegated commune
22
+ # - :commune_associee: Associated commune
23
+ # @note Default value is :commune_actuelle
24
+ attr_reader :code, :nom, :zone, :region_code, :departement_code, :commune_type
25
+
26
+ # rubocop:disable Metrics/ParameterLists
27
+ # @param code [String] the INSEE code of the commune
28
+ # @param nom [String] the name of the commune
29
+ # @param zone [String] the zone of the commune ("metro", "drom", "com")
30
+ # @param region_code [String] the INSEE code of the region
31
+ # @param departement_code [String] the INSEE code of the department
32
+ # @param commune_type [Symbol] the type of the commune (default: :commune_actuelle)
33
+ def initialize(code:, nom:, zone:, region_code:, departement_code:, commune_type: :commune_actuelle)
34
+ @code = code
35
+ @nom = nom
36
+ @zone = zone
37
+ @region_code = region_code
38
+ @departement_code = departement_code
39
+ @commune_type = commune_type
40
+ end
41
+ # rubocop:enable Metrics/ParameterLists
42
+
43
+ # @return [Array<Commune>] a collection of all communes
44
+ def self.all
45
+ @all ||= Parser.new('communes').data.map do |commune_data|
46
+ Commune.new(
47
+ code: commune_data["code"],
48
+ nom: commune_data["nom"],
49
+ zone: commune_data["zone"],
50
+ region_code: commune_data["region"],
51
+ departement_code: commune_data["departement"],
52
+ commune_type: commune_data["type"]&.gsub("-", "_")&.to_sym
53
+ )
54
+ end
55
+ end
56
+
57
+ # @return [Array<Commune>] a collection of all communes _actuelles_
58
+ def self.actuelles
59
+ @actuelles ||= where(commune_type: :commune_actuelle)
60
+ end
61
+
62
+ # @raise [NotFoundError] if no region is found for the code
63
+ # @return [Region] the region of the commune
64
+ def region
65
+ @region ||= DecoupageAdministratif::Region.find(@region_code)
66
+ end
67
+
68
+ # @raise [NotFoundError] if no department is found for the code
69
+ # @return [Departement] the department of the commune
70
+ def departement
71
+ @departement ||= DecoupageAdministratif::Departement.find(@departement_code)
72
+ end
73
+
74
+ # @return [Epci, nil] the EPCI of the commune, if it belongs to one
75
+ def epci
76
+ found_epci = DecoupageAdministratif::Epci.all.find do |epci|
77
+ epci.membres.any? { |m| m["code"] == @code }
78
+ end
79
+ found_epci.is_a?(DecoupageAdministratif::Epci) ? (@epci ||= found_epci) : nil
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoupageAdministratif
4
+ class Config
5
+ class << self
6
+ # @return [String] the directory where data files are stored
7
+ def data_directory
8
+ @data_directory ||= determine_data_directory
9
+ end
10
+
11
+ # @param path [String] the path to set as data directory
12
+ attr_writer :data_directory
13
+
14
+ # @return [String] the directory where embedded data files are stored
15
+ def embedded_data_directory
16
+ @embedded_data_directory ||= File.join(gem_root, 'data')
17
+ end
18
+
19
+ # @return [String] the root directory of the gem
20
+ def gem_root
21
+ @gem_root ||= File.expand_path('../..', __dir__)
22
+ end
23
+
24
+ private
25
+
26
+ def determine_data_directory
27
+ ENV['DECOUPAGE_DATA_DIR'] ||
28
+ user_data_directory ||
29
+ fallback_directory
30
+ end
31
+
32
+ def user_data_directory
33
+ return nil unless Dir.home
34
+
35
+ File.join(Dir.home, '.local', 'share', 'decoupage_administratif')
36
+ end
37
+
38
+ def fallback_directory
39
+ if defined?(Rails) && Rails.respond_to?(:root)
40
+ Rails.root.join('tmp', 'decoupage_administratif').to_s
41
+ else
42
+ File.join(Dir.tmpdir, 'decoupage_administratif')
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoupageAdministratif
4
+ class Departement
5
+ extend BaseModel
6
+ include TerritoryExtensions
7
+ # @!attribute [r] code
8
+ # @return [String] INSEE code of the department
9
+ # @!attribute [r] nom
10
+ # @return [String] Name of the department
11
+ # @!attribute [r] zone
12
+ # @return [String] Zone of the department ("metro", "drom", "com")
13
+ # @!attribute [r] code_region
14
+ # @return [String] INSEE code of the region
15
+ attr_reader :code, :nom, :zone, :code_region
16
+
17
+ # @param code [String] the INSEE code of the department
18
+ # @param nom [String] the name of the department
19
+ # @param zone [String] the zone of the department ("metro", "drom", "com")
20
+ # @param code_region [String] the INSEE code of the region
21
+ def initialize(code:, nom:, zone:, code_region:)
22
+ @code = code
23
+ @nom = nom
24
+ @zone = zone
25
+ @code_region = code_region
26
+ end
27
+
28
+ # @return [Array<Departement>] a collection of all departments
29
+ def self.all
30
+ @all ||= Parser.new('departements').data.map do |departement_data|
31
+ DecoupageAdministratif::Departement.new(
32
+ code: departement_data["code"],
33
+ nom: departement_data["nom"],
34
+ zone: departement_data["zone"],
35
+ code_region: departement_data["region"]
36
+ )
37
+ end
38
+ end
39
+
40
+ # @return [Array<Commune>] a collection of all actual communes in the department
41
+ def communes
42
+ @communes ||= DecoupageAdministratif::Commune.where(departement_code: @code, commune_type: :commune_actuelle)
43
+ end
44
+
45
+ # @raise [NotFoundError] if no region is found for the code
46
+ # @return [Region] the region of the department
47
+ def region
48
+ @region ||= DecoupageAdministratif::Region.find(@code_region)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoupageAdministratif
4
+ class Epci
5
+ extend BaseModel
6
+ include TerritoryExtensions
7
+
8
+ # @!attribute [r] code
9
+ # @return [String] SIREN code of the EPCI
10
+ # @!attribute [r] nom
11
+ # @return [String] Name of the EPCI
12
+ # @!attribute [r] membres
13
+ # @return [Array<Hash>] Members of the EPCI, each member is a hash with "nom" and "code" keys
14
+ attr_reader :code, :nom, :membres
15
+
16
+ # @param code [String] the SIREN code of the EPCI
17
+ # @param nom [String] the name of the EPCI
18
+ # @param membres [Array<Hash>] the members of the EPCI, each member is a hash with "nom" and "code" keys
19
+ def initialize(code:, nom:, membres: [])
20
+ @code = code
21
+ @nom = nom
22
+ @membres = membres
23
+ end
24
+
25
+ # @return [Array<Epci>] a collection of all EPCI
26
+ def self.all
27
+ Parser.new('epci').data.map do |epci_data|
28
+ Epci.new(
29
+ code: epci_data["code"],
30
+ nom: epci_data["nom"],
31
+ membres: epci_data["membres"].map { |membre| membre.slice("nom", "code") }
32
+ )
33
+ end
34
+ end
35
+
36
+ # Search for an EPCI that includes all the specified codes
37
+ # @param codes [Array<String>] an array of commune codes
38
+ # @return [Array<Epci>] a collection of EPCI that include all the specified codes
39
+ def self.search_by_communes_codes(codes)
40
+ all.select do |epci|
41
+ epci.membres.map do |m|
42
+ codes.include?(m['code'])
43
+ end.all?
44
+ end
45
+ end
46
+
47
+ # @return [Array<Commune>] a collection of all communes that are members of the EPCI
48
+ # @raise [NotFoundError] if a commune code is not found
49
+ def communes
50
+ @communes ||= @membres.map do |membre|
51
+ DecoupageAdministratif::Commune.find(membre["code"])
52
+ end.compact
53
+ end
54
+
55
+ # @return [Array<Region>] an array of regions that the EPCI communes belong to
56
+ # Sometimes an EPCI can have communes from different regions.
57
+ def regions
58
+ @regions ||= communes.map(&:region).uniq
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module DecoupageAdministratif
6
+ class Parser
7
+ # @!attribute [r] data
8
+ # @return [Array<Hash>] Parsed data from the JSON file for the given model
9
+ attr_reader :data
10
+
11
+ # @param model [String] the name of the model to parse (e.g., 'communes', 'departements', 'regions')
12
+ # @return [Parser] a new Parser instance
13
+ # @note Only expected model names should be used to avoid loading unwanted files. The file path is constructed from the model name.
14
+ def initialize(model)
15
+ @model = model
16
+ @file_path = File.join(DecoupageAdministratif::Config.data_directory, "#{@model}.json")
17
+ load_data
18
+ end
19
+
20
+ private
21
+
22
+ def load_data
23
+ file = File.read(@file_path)
24
+ @data = JSON.parse(file)
25
+ rescue Errno::ENOENT
26
+ # Try to load from embedded data if external file not found
27
+ @file_path = File.join(DecoupageAdministratif::Config.embedded_data_directory, "#{@model}.json")
28
+ begin
29
+ file = File.read(@file_path)
30
+ @data = JSON.parse(file)
31
+ rescue Errno::ENOENT
32
+ raise Error,
33
+ "File #{@file_path} does not exist. You can update the data with 'rake decoupage_administratif:update'"
34
+ rescue JSON::ParserError
35
+ raise Error, "File #{@model}.json is not valid JSON"
36
+ end
37
+ rescue JSON::ParserError
38
+ raise Error, "File #{@model}.json is not valid JSON"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+
5
+ module DecoupageAdministratif
6
+ class Railtie < Rails::Railtie
7
+ rake_tasks do
8
+ load 'tasks/install.rake'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoupageAdministratif
4
+ class Region
5
+ extend BaseModel
6
+ include TerritoryExtensions
7
+
8
+ # @!attribute [r] code
9
+ # @return [String] INSEE code of the region
10
+ # @!attribute [r] nom
11
+ # @return [String] Name of the region
12
+ # @!attribute [r] zone
13
+ # @return [String] Zone of the region ("metro", "drom", "com")
14
+ attr_reader :code, :nom, :zone
15
+
16
+ # @param code [String] the INSEE code of the region
17
+ # @param nom [String] the name of the region
18
+ # @param zone [String] the zone of the region ("metro", "drom", "com")
19
+ def initialize(code:, nom:, zone:)
20
+ @code = code
21
+ @nom = nom
22
+ @zone = zone
23
+ end
24
+
25
+ # @return [Array<Region>] a collection of all regions
26
+ def self.all
27
+ @all ||= Parser.new('regions').data.map do |region_data|
28
+ Region.new(
29
+ code: region_data["code"],
30
+ nom: region_data["nom"],
31
+ zone: region_data["zone"]
32
+ )
33
+ end
34
+ end
35
+
36
+ # @return [Array<Departement>] a collection of all departments in the region
37
+ def departements
38
+ @departements ||= DecoupageAdministratif::Departement.all.select do |departement|
39
+ departement.code_region == @code
40
+ end
41
+ end
42
+
43
+ # @return [Array<Commune>] a collection of all actual communes in the region
44
+ def communes
45
+ @communes ||= DecoupageAdministratif::Commune.all.select do |commune|
46
+ commune.region_code == @code && commune.commune_type == :commune_actuelle
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoupageAdministratif
4
+ class Search
5
+ # @!attribute [r] codes
6
+ # @return [Array<String>, nil] List of INSEE codes used for the search
7
+ # @!attribute [r] regions
8
+ # @return [Array<Region>] Regions found by the search
9
+ # @!attribute [r] departements
10
+ # @return [Array<Departement>] Departments found by the search
11
+ # @!attribute [r] epcis
12
+ # @return [Array<Epci>] EPCIs found by the search
13
+ # @!attribute [r] communes
14
+ # @return [Array<Commune>] Communes found by the search
15
+ attr_reader :codes, :regions, :departements, :epcis, :communes
16
+
17
+ def initialize(codes = nil)
18
+ @codes = codes&.uniq
19
+ initialize_class_caches
20
+ end
21
+
22
+ # Search for territories by municipality.
23
+ # If the list of municipalities represents a department or an EPCI, the corresponding territories are displayed.
24
+ # If the codes do not correspond to any territory, a list of municipalities is returned.
25
+ # @return [Hash] a hash containing the regions, departments, EPCI, and communes found
26
+ # @note Returns empty arrays if no territories are found for the given codes.
27
+ def by_insee_codes
28
+ @codes = group_by_departement
29
+ @codes = find_communes_by_codes
30
+
31
+ search_for_departements
32
+ search_for_region
33
+ search_for_epcis
34
+ search_for_communes
35
+
36
+ {
37
+ regions: @regions,
38
+ departements: @departements,
39
+ epcis: @epcis,
40
+ communes: @communes
41
+ }
42
+ end
43
+
44
+ # Return the territories associated with a given INSEE code.
45
+ # @param code_insee [String] the INSEE code of the commune
46
+ # @return [Hash] a hash containing the EPCI, department, and region associated with the commune
47
+ def find_territories_by_commune_insee_code(code_insee)
48
+ # Use cache if available, fallback to find_by for compatibility with tests
49
+ commune = @@communes_cache&.[](code_insee) || DecoupageAdministratif::Commune.find_by(code: code_insee)
50
+ return {} if commune.nil?
51
+
52
+ {
53
+ epci: commune.epci,
54
+ departement: commune.departement,
55
+ region: commune.region
56
+ }
57
+ end
58
+
59
+ private
60
+
61
+ # Class-level caches for performance optimization
62
+ # rubocop:disable Style/ClassVars
63
+ @@departements_cache = nil
64
+ @@communes_cache = nil
65
+ # rubocop:enable Style/ClassVars
66
+
67
+ # Initialize class-level caches for performance optimization
68
+ # @return [void]
69
+ def initialize_class_caches
70
+ return if @@departements_cache && @@communes_cache
71
+
72
+ # rubocop:disable Style/ClassVars
73
+ @@departements_cache = DecoupageAdministratif::Departement.all.each_with_object({}) { |dept, hash| hash[dept.code] = dept }
74
+ @@communes_cache = DecoupageAdministratif::Commune.actuelles.each_with_object({}) { |commune, hash| hash[commune.code] = commune }
75
+ # rubocop:enable Style/ClassVars
76
+ end
77
+
78
+ # Group the codes by department.
79
+ # @return [Hash<Departement, Array<Commune>>] Hash with departments as keys and their communes as values
80
+ def group_by_departement
81
+ # Group the codes by DecoupageAdministratif::Departement
82
+ # and DecoupageAdministratif::Communes as values
83
+ @codes.group_by do |code|
84
+ dept_code = code[0..1] == "97" ? code[0..2] : code[0..1]
85
+ @@departements_cache[dept_code]
86
+ end
87
+ end
88
+
89
+ # Find communes by their codes.
90
+ # @return [Array<Commune>] List of communes matching the codes
91
+ def find_communes_by_codes
92
+ @codes.transform_values do |codes_insee|
93
+ codes_insee.filter_map do |code|
94
+ commune = @@communes_cache[code]
95
+ next if commune.nil?
96
+
97
+ commune.commune_type == :commune_actuelle ? commune : nil
98
+ end
99
+ end
100
+ end
101
+
102
+ # Search for departments matching the codes.
103
+ # @return [void]
104
+ def search_for_departements
105
+ @departements = []
106
+ return if only_nil_key?
107
+
108
+ departements_to_delete = []
109
+ @codes.each do |departement, communes|
110
+ next unless should_add_departement?(departement, communes)
111
+
112
+ @departements << departement
113
+ departements_to_delete << departement
114
+ end
115
+ departements_to_delete.each { |dep_code| @codes.delete(dep_code) }
116
+ end
117
+
118
+ # Check if only one key and it is nil
119
+ # @return [Boolean]
120
+ def only_nil_key?
121
+ @codes.keys.uniq.count == 1 && @codes.keys.first.nil?
122
+ end
123
+
124
+ # Should add departement to result?
125
+ # @param departement [Departement, nil]
126
+ # @param communes [Array<Commune>]
127
+ # @return [Boolean]
128
+ def should_add_departement?(departement, communes)
129
+ return false if departement.nil?
130
+
131
+ departement.communes.count == communes.count && departement.communes.map(&:code).sort == communes.map(&:code).sort
132
+ end
133
+
134
+ # Search for regions matching the codes.
135
+ # @return [void]
136
+ def search_for_region
137
+ @regions = []
138
+ return if @departements.empty?
139
+
140
+ regions = DecoupageAdministratif::Region.all
141
+ regions.each do |region|
142
+ next unless region.departements.all? do |departement|
143
+ @departements.map(&:code).include? departement.code
144
+ end
145
+
146
+ @regions << region
147
+ region.departements.each do |departement|
148
+ @departements.delete(departement)
149
+ end
150
+ end
151
+ end
152
+
153
+ # Search for EPCIs matching the codes.
154
+ # @return [void]
155
+ def search_for_epcis
156
+ @epcis = []
157
+ return if @codes.keys.uniq.count == 1 && @codes.keys.first.nil?
158
+
159
+ @codes.each_value do |communes|
160
+ # Find EPCIs that match all communes in the current group
161
+ @epcis = DecoupageAdministratif::Epci.search_by_communes_codes(communes.map(&:code))
162
+ @epcis.each do |epci|
163
+ communes.reject! do |commune|
164
+ epci.communes.include?(commune)
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ # Search for communes matching the codes.
171
+ # @return [void]
172
+ def search_for_communes
173
+ @communes = []
174
+ return if @codes.keys.uniq.count == 1 && @codes.keys.first.nil?
175
+
176
+ @codes.each_value do |communes|
177
+ communes.map { |commune| @communes << commune }
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DecoupageAdministratif
4
+ module TerritoryExtensions
5
+ # Check if this territory intersects with a list of commune INSEE codes
6
+ # @param commune_insee_codes [Array<String>] array of commune INSEE codes to check against
7
+ # @return [Boolean] true if territory intersects with any of the provided codes
8
+ def territory_intersects_with_insee_codes?(commune_insee_codes)
9
+ territory_strategy.intersects_with_insee_codes?(commune_insee_codes)
10
+ end
11
+
12
+ # Get commune INSEE codes for this territory
13
+ # @return [Array<String>] array of commune INSEE codes covered by this territory
14
+ def territory_insee_codes
15
+ @territory_insee_codes ||= territory_strategy.insee_codes
16
+ end
17
+
18
+ private
19
+
20
+ def territory_strategy
21
+ @territory_strategy ||= case self.class.name.split('::').last
22
+ when 'Commune'
23
+ TerritoryStrategies::CommuneStrategy.new(self)
24
+ when 'Departement'
25
+ TerritoryStrategies::DepartementStrategy.new(self)
26
+ when 'Region'
27
+ TerritoryStrategies::RegionStrategy.new(self)
28
+ when 'Epci'
29
+ TerritoryStrategies::EpciStrategy.new(self)
30
+ else
31
+ raise NotImplementedError, "Territory strategy not implemented for #{self.class.name}"
32
+ end
33
+ end
34
+ end
35
+ end