Module: Psgc

Defined in:
lib/psgc.rb,
lib/psgc/cli.rb,
lib/generators/psgc/seed_generator.rb

Defined Under Namespace

Modules: Generators Classes: CLI, Error

Constant Summary collapse

VERSION =
"0.2.0"

Class Method Summary collapse

Class Method Details

.barangaysObject



28
29
30
# File 'lib/psgc.rb', line 28

def self.barangays
  @barangays || @data_mutex.synchronize { @barangays ||= load_data("barangays") }
end

.cities_municipalitiesObject



24
25
26
# File 'lib/psgc.rb', line 24

def self.cities_municipalities
  @cities_municipalities || @data_mutex.synchronize { @cities_municipalities ||= load_data("cities_municipalities") }
end

.collection_for(level) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
# File 'lib/psgc.rb', line 195

def self.collection_for(level)
  normalized = normalize_level(level)

  case normalized
  when :regions then regions
  when :provinces then provinces
  when :cities_municipalities then cities_municipalities
  when :barangays then barangays
  else raise ArgumentError, "unknown level: #{level}. Valid: :regions, :provinces, :cities, :cities_municipalities, :barangays"
  end
end

.data_dirObject



10
11
12
# File 'lib/psgc.rb', line 10

def self.data_dir
  File.expand_path("../../data", __FILE__)
end

.export_csv(level: :regions, include_headers: true) ⇒ Object



207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/psgc.rb', line 207

def self.export_csv(level: :regions, include_headers: true)
  require "csv"
  collection = collection_for(level)

  headers = collection.first.keys.map(&:to_s)

  CSV.generate do |csv|
    csv << headers if include_headers
    collection.each do |item|
      csv << headers.map { |h| item[h.to_sym] }
    end
  end
end

.export_geojson(level: :regions) ⇒ String

Note: geometry is null because PSGC data has no geographic coordinates.

Returns:

  • (String)

    GeoJSON FeatureCollection



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/psgc.rb', line 230

def self.export_geojson(level: :regions)
  collection = collection_for(level)

  features = collection.map do |item|
    props = item.dup
    props.delete(:code)
    {
      type: "Feature",
      id: item[:code],
      geometry: nil,
      properties: props
    }
  end

  {
    type: "FeatureCollection",
    features: features
  }.to_json
end

.export_yaml(level: :regions) ⇒ Object



221
222
223
224
225
226
# File 'lib/psgc.rb', line 221

def self.export_yaml(level: :regions)
  require "yaml"
  collection = collection_for(level)

  { normalize_level(level) => collection }.to_yaml
end

.find(collection, code: nil, name: nil, **attrs) ⇒ Hash?

Finds first match in collection using AND semantics. All non-nil criteria must match for a result. Name matching is case-insensitive substring. Code attributes (*_code) use bidirectional prefix matching:

"v.start_with?(item[k]) || item[k].start_with?(v)"
e.g., region_code "14" matches stored "1400000000" and vice versa.

Parameters:

  • collection (Array<Hash>)

    data collection

  • code (String, nil) (defaults to: nil)

    exact PSGC code

  • name (String, nil) (defaults to: nil)

    case-insensitive substring match

  • attrs (Hash)

    additional match criteria

Returns:

  • (Hash, nil)

    first matching item or nil



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/psgc.rb', line 71

def self.find(collection, code: nil, name: nil, **attrs)
  collection.each do |item|
    matches = true
    matches &&= item[:code] == code if code
    matches &&= item[:name].to_s.downcase.include?(name.to_s.downcase) if name
    attrs.each do |k, v|
      next unless v
      if k.to_s.end_with?("_code")
        matches &&= v.to_s.start_with?(item[k].to_s) || item[k].to_s.start_with?(v.to_s)
      else
        matches &&= item[k] == v
      end
    end
    return item if matches
  end
  nil
end

.find_barangay(code: nil, name: nil, city_municipality_code: nil) ⇒ Object



54
55
56
57
# File 'lib/psgc.rb', line 54

def self.find_barangay(code: nil, name: nil, city_municipality_code: nil)
  return nil unless code || name || city_municipality_code
  find(barangays, code: code, name: name, city_municipality_code: city_municipality_code)
end

.find_city_municipality(code: nil, name: nil, province_code: nil) ⇒ Object



49
50
51
52
# File 'lib/psgc.rb', line 49

def self.find_city_municipality(code: nil, name: nil, province_code: nil)
  return nil unless code || name || province_code
  find(cities_municipalities, code: code, name: name, province_code: province_code)
end

.find_province(code: nil, name: nil, region_code: nil) ⇒ Object



44
45
46
47
# File 'lib/psgc.rb', line 44

def self.find_province(code: nil, name: nil, region_code: nil)
  return nil unless code || name || region_code
  find(provinces, code: code, name: name, region_code: region_code)
end

.find_region(code: nil, name: nil) ⇒ Object



39
40
41
42
# File 'lib/psgc.rb', line 39

def self.find_region(code: nil, name: nil)
  return nil unless code || name
  find(regions, code: code, name: name)
end

.hierarchy(code) ⇒ Hash{Symbol => Hash, nil}

Traverses PSGC hierarchy for a given code. Uses prefix matching because parent codes are prefixes of child codes (e.g., city code is prefix of barangay code).

Parameters:

  • code (String, Integer)

    10-digit PSGC code

Returns:

  • (Hash{Symbol => Hash, nil})

    hash with :code and found geographic levels



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/psgc.rb', line 95

def self.hierarchy(code)
  return nil unless code.to_s.match?(/^\d{10}$/)
  code = code.to_s

  result = { code: code }

  if code.end_with?("000000")
    result[:region] = find_region(code: code)
  elsif code.end_with?("0000")
    result[:province] = find_province(code: code)
    result[:city_municipality] = find_city_municipality(code: code) unless result[:province]
  elsif code.end_with?("00")
    result[:city_municipality] = find_city_municipality(code: code)
  else
    result[:barangay] = find_barangay(code: code)
  end

  if result[:barangay]
    city = cities_municipalities.find { |c| c[:code].start_with?(result[:barangay][:city_municipality_code]) }
    result[:city_municipality] = city
    if city
      province = provinces.find { |p| p[:code].start_with?(city[:province_code]) }
      result[:province] = province
    end
  elsif result[:city_municipality]
    province = provinces.find { |p| p[:code].start_with?(result[:city_municipality][:province_code]) }
    result[:province] = province
  end

  if result[:province]
    region = regions.find { |r| r[:code].start_with?(result[:province][:region_code]) }
    result[:region] = region
  end

  result[:region] ||= regions.find { |r| r[:code].start_with?(code[0, 2]) }

  result
end

.load_data(type) ⇒ Object



32
33
34
35
36
37
# File 'lib/psgc.rb', line 32

def self.load_data(type)
  file_path = File.join(data_dir, "#{type}.json")
  return [] unless File.exist?(file_path)

  JSON.parse(File.read(file_path), symbolize_names: true)
end

.normalize_level(level) ⇒ Symbol

Returns normalized level (:cities -> :cities_municipalities).

Parameters:

  • level (Symbol)

    geographic level

Returns:

  • (Symbol)

    normalized level (:cities -> :cities_municipalities)



188
189
190
191
192
193
# File 'lib/psgc.rb', line 188

def self.normalize_level(level)
  case level
  when :cities then :cities_municipalities
  else level
  end
end

.provincesObject



20
21
22
# File 'lib/psgc.rb', line 20

def self.provinces
  @provinces || @data_mutex.synchronize { @provinces ||= load_data("provinces") }
end

.regionsObject



16
17
18
# File 'lib/psgc.rb', line 16

def self.regions
  @regions || @data_mutex.synchronize { @regions ||= load_data("regions") }
end

.search(query, levels: nil, limit: nil) ⇒ Hash{Symbol => Array}

Returns hash with requested level keys and matching records.

Parameters:

  • query (String)

    search term (case-insensitive substring match)

  • levels (Array<Symbol>) (defaults to: nil)

    which levels to search (:regions, :provinces, :cities, :cities_municipalities, :barangays)

  • limit (Integer) (defaults to: nil)

    max matches per level (not total)

Returns:

  • (Hash{Symbol => Array})

    hash with requested level keys and matching records



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/psgc.rb', line 138

def self.search(query, levels: nil, limit: nil)
  return {} unless query && !query.to_s.strip.empty?

  query_down = query.to_s.downcase
  levels ||= [:regions, :provinces, :cities_municipalities, :barangays]

  result = {}
  levels.each do |level|
    collection = collection_for(level)

    matches = collection.select { |item| item[:name].to_s.downcase.include?(query_down) }
    matches = matches.first(limit) if limit
    result[level] = matches
  end

  result
end

.statsHash{Symbol => Integer}

Returns counts by level.

Returns:

  • (Hash{Symbol => Integer})

    counts by level



177
178
179
180
181
182
183
184
# File 'lib/psgc.rb', line 177

def self.stats
  {
    regions: regions.length,
    provinces: provinces.length,
    cities_municipalities: cities_municipalities.length,
    barangays: barangays.length
  }
end

.valid?(code) ⇒ Boolean

Returns true if code exists in any level.

Parameters:

  • code (String, Integer)

    10-digit PSGC code

Returns:

  • (Boolean)

    true if code exists in any level



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/psgc.rb', line 158

def self.valid?(code)
  return false unless code && code.to_s.match?(/^\d{10}$/)

  code_str = code.to_s

  if code_str.end_with?("000000")
    regions.any? { |r| r[:code] == code_str }
  elsif code_str.end_with?("0000")
    provinces.any? { |p| p[:code] == code_str } ||
      cities_municipalities.any? { |c| c[:code] == code_str }
  elsif code_str.end_with?("00")
    cities_municipalities.any? { |c| c[:code] == code_str } ||
      barangays.any? { |b| b[:code] == code_str }
  else
    barangays.any? { |b| b[:code] == code_str }
  end
end