Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CLAUDE.md — ruby_german_zip_codes

## Was dieses Projekt ist

Ruby-Gem für die Suche nach deutschen Postleitzahlen (PLZ). Gibt zu einer PLZ Stadt, Landkreis/Bezirk und Bundesland zurück. Kein US-Support — das ist ein Fork des ursprünglichen `zip-codes`-Gems, der ausschließlich auf Deutschland fokussiert ist.

## Projektstruktur

```
lib/
zip-codes.rb # Haupt-Einstiegspunkt, öffentliche API
zip-codes/
city_map.rb # Datenstruktur für den PLZ-Hash (YAML ↔ Ruby)
converter.rb # CSV → YAML Konverter (für Datenpflege)
version.rb # VERSION-Konstante
data/
DE.yml # Produktiv-Datenbank (~8.000 PLZ, ~1 MB)
de.csv # Quelldaten (OSM-Export, Basis für DE.yml)
DE_old.yml # Veraltete Version – kann entfernt werden
US.yml # Nicht genutzt – Überbleibsel vom Original-Fork
```

## Datenformat

`DE.yml` enthält einen Hash mit PLZ-String als Key:

```yaml
'20535':
:osm_id: '62782'
:code: '20535'
:city: Hamburg
:county: Hamburg-Mitte
:state: Hamburg
```

Bei mehreren Städten pro PLZ wird `city` als Semikolon-separierter String gespeichert (`"Stadt1;Stadt2"`).

## Datenpflege: CSV → YAML neu generieren

```bash
bundle exec rake convert
```

Liest `lib/data/de.csv` und schreibt `lib/data/DE.yml` neu. Das CSV hat folgende Spalten (Semikolon-getrennt):

| Index | Feld |
|-------|----------|
| 0 | osm_id |
| 1 | city |
| 2 | code |
| 3 | county |
| 4 | state |

## Wichtige Hinweise

- Die YAML-Datei wird beim ersten Aufruf vollständig in den Speicher geladen (Memoize via `@db`). In Rails-Apps `ZipCodes.load` im Initializer aufrufen.
- Der `city_index` (Reverse-Index Stadt → [PLZ]) wird lazy beim ersten `codes`-Aufruf aufgebaut und ebenfalls gecacht.
- `YAML.load_file` mit `permitted_classes: [Symbol]` ist notwendig, weil `DE.yml` Symbol-Keys enthält (Psych 4 / Ruby 3.1+).

## Tests ausführen

```bash
bundle exec rspec
```

Code-Style prüfen:

```bash
bundle exec standardrb
```

## CI

GitHub Actions (`.github/workflows/ci.yml`): läuft bei Push auf `main` und bei PRs. Führt StandardRb und RSpec aus.
28 changes: 13 additions & 15 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@ PATH
remote: .
specs:
zip-codes (0.2.1)
csv

GEM
remote: https://rubygems.org/
specs:
ast (2.4.3)
csv (3.3.5)
diff-lcs (1.6.2)
json (2.18.1)
language_server-protocol (3.17.0.5)
json (2.20.0)
language_server-protocol (3.17.0.6)
lint_roller (1.1.0)
parallel (1.27.0)
parser (3.3.10.2)
parallel (2.1.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
prism (1.9.0)
racc (1.8.1)
rainbow (3.1.1)
rake (13.3.1)
regexp_parser (2.11.3)
rake (13.4.2)
regexp_parser (2.12.0)
rspec (3.13.2)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
Expand All @@ -31,33 +29,33 @@ GEM
rspec-expectations (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.7)
rspec-mocks (3.13.8)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.7)
rubocop (1.84.2)
rubocop (1.87.0)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parallel (>= 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.49.0)
rubocop-ast (1.50.0)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-performance (1.26.1)
lint_roller (~> 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (1.13.0)
standard (1.54.0)
standard (1.55.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.84.0)
rubocop (~> 1.87.0)
standard-custom (~> 1.0.0)
standard-performance (~> 1.8)
standard-custom (1.0.2)
Expand All @@ -74,7 +72,7 @@ PLATFORMS
ruby

DEPENDENCIES
bundler (> 1.3)
bundler (>= 2.0)
rake
rspec (~> 3.0)
standard
Expand Down
74 changes: 56 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,78 @@
<img alt="Ruby Code Style" src="https://img.shields.io/badge/Ruby_Code_Style-standard-brightgreen.svg" />
</a>

Simple gem to get city, state, and time zone for a given zip code. It has a yaml database bundled with it, so you need several mb of memory for the whole hash.
Ruby-Gem zur Suche nach deutschen Postleitzahlen (PLZ). Gibt zu einer PLZ die zugehörige Stadt, den Landkreis bzw. Bezirk und das Bundesland zurück. Der vollständige Datensatz (~8.000 PLZ) ist als YAML-Datei eingebettet und wird beim ersten Zugriff in den Arbeitsspeicher geladen.

## Installation

Add this line to your application's Gemfile:
In der `Gemfile` eintragen:

gem 'zip-codes'
```ruby
gem 'zip-codes'
```

And then execute:
Dann ausführen:

$ bundle
$ bundle install

Or install it yourself as:
Oder direkt installieren:

$ gem install zip-codes

## Usage
## Verwendung

### Exakte Suche

```ruby
ZipCodes.identify('20535')
# => { city: "Hamburg", code: "20535", county: "Hamburg-Mitte", osm_id: "62782", state: "Hamburg" }

ZipCodes.identify('99999')
# => nil
```

### Präfix-Suche

```ruby
ZipCodes.identify('205', like_search: true)
# => { "20535" => { city: "Hamburg", ... }, "20537" => { ... }, ... }

# Kurzform:
ZipCodes.like('205')
```

### Alle PLZ einer Stadt

```ruby
ZipCodes.identify('30301')
# => {:state_code=>"GA", :state_name=>"Georgia", :city=>"Atlanta", :time_zone=>"America/New_York"}
# First run will take a while, as the yaml has to be loaded
ZipCodes.codes('Hamburg')
# => ["20095", "20097", ..., "22769"] # 98 Einträge
```

If you are using Rails, you can load the hash on app startup for production and staging.
### Rails: Vorladen beim Start

Damit die YAML-Datei nicht beim ersten Request geparst wird, kann die Datenbank in einem Initializer vorgeladen werden:

```ruby
# config/initializers/load_zip_codes.rb
# config/initializers/zip_codes.rb
ZipCodes.load unless Rails.env.development?
```

## Contributing
## Datenfelder

| Feld | Beispiel | Beschreibung |
|----------|--------------------|--------------------------|
| `code` | `"20535"` | PLZ (5-stelliger String) |
| `city` | `"Hamburg"` | Stadt / Gemeinde |
| `county` | `"Hamburg-Mitte"` | Landkreis oder Bezirk |
| `state` | `"Hamburg"` | Bundesland |
| `osm_id` | `"62782"` | OpenStreetMap-Relation |

Die Daten basieren auf OpenStreetMap-Daten.

## Mitwirken

1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
1. Fork erstellen
2. Feature-Branch anlegen (`git checkout -b mein-feature`)
3. Änderungen committen (`git commit -am 'Feature hinzufügen'`)
4. Branch pushen (`git push origin mein-feature`)
5. Pull Request öffnen
18 changes: 7 additions & 11 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ require "bundler/gem_tasks"
require "yaml"

task :convert do
codes = {}
File.open("lib/data/US.txt").each_line do |line|
array = line.split('\t')
codes[array[1]] = {
state_code: array[4],
state_name: array[3],
city: array[2]
}
end

File.write("lib/data/US.yml", codes.to_yaml)
require_relative "lib/zip-codes/city_map"
require_relative "lib/zip-codes/converter"
converter = ZipCodes::Converter.new("DE", load: false)
converter.convert
converter.store!
city_map = converter.map
puts "Converted #{city_map.map.size} zip codes → lib/data/DE.yml"
end
21 changes: 14 additions & 7 deletions lib/zip-codes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,30 @@ def identify(code, like_search: false)
end

def codes(city)
db.select { |key, hash| hash[:city] == city }.keys
city_index.fetch(city, []).dup
end

def like(code)
db.select { |key, hash| key && key.index(code) == 0 }
db.select { |key, _| key.start_with?(code) }
end

def db
@db ||= begin
this_file = __dir__
data = File.join(this_file, "data", "DE.yml")
YAML.load(File.open(data))
end
@db ||= YAML.load_file(
File.join(__dir__, "data", "DE.yml"),
permitted_classes: [Symbol]
)
end

def load
db
end

private

def city_index
@city_index ||= db.each_with_object({}) do |(code, data), idx|
(idx[data[:city]] ||= []) << code
end
end
end
end
Loading
Loading