swissmatch-location 0.0.1
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.
- data/LICENSE.txt +8 -0
- data/README.markdown +96 -0
- data/Rakefile +10 -0
- data/data/swissmatch/plz_c_20120723.txt +2498 -0
- data/data/swissmatch/plz_p1_20120723.txt +5331 -0
- data/data/swissmatch/plz_p2_20120723.txt +1828 -0
- data/lib/swissmatch/canton.rb +84 -0
- data/lib/swissmatch/cantons.rb +121 -0
- data/lib/swissmatch/communities.rb +126 -0
- data/lib/swissmatch/community.rb +88 -0
- data/lib/swissmatch/datafiles.rb +333 -0
- data/lib/swissmatch/loaderror.rb +26 -0
- data/lib/swissmatch/location/autoload.rb +8 -0
- data/lib/swissmatch/location/ruby.rb +43 -0
- data/lib/swissmatch/location/version.rb +15 -0
- data/lib/swissmatch/location.rb +249 -0
- data/lib/swissmatch/name.rb +47 -0
- data/lib/swissmatch/zip.rb +40 -0
- data/lib/swissmatch/zipcode.rb +303 -0
- data/lib/swissmatch/zipcodes.rb +255 -0
- data/swissmatch-location.gemspec +44 -0
- data/test/data/plz_c_20120000.txt +2510 -0
- data/test/data/plz_p1_20120000.txt +5333 -0
- data/test/data/plz_p1up_20120000.txt +29 -0
- data/test/data/plz_p2_20120000.txt +1830 -0
- data/test/lib/helper.rb +38 -0
- data/test/lib/test/swissmatch_location_fixtures.rb +6 -0
- data/test/runner.rb +20 -0
- data/test/unit/lib/swissmatch/location.rb +10 -0
- data/test/unit/lib/swissmatch/zip_codes.rb +21 -0
- metadata +112 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
module SwissMatch
|
6
|
+
|
7
|
+
# Represents a swiss canton.
|
8
|
+
class Canton
|
9
|
+
|
10
|
+
# @return [String]
|
11
|
+
# The two letter abbreviation of the cantons name as used on license plates.
|
12
|
+
attr_reader :license_tag
|
13
|
+
|
14
|
+
# @param [String] license_tag
|
15
|
+
# The two letter abbreviation of the cantons name as used on license plates.
|
16
|
+
# @param [String] name
|
17
|
+
# The official name of the canton in the local language.
|
18
|
+
# @param [String] name_de
|
19
|
+
# The official name of the canton in german.
|
20
|
+
# @param [String] name_fr
|
21
|
+
# The official name of the canton in french.
|
22
|
+
# @param [String] name_it
|
23
|
+
# The official name of the canton in italian.
|
24
|
+
# @param [String] name_rt
|
25
|
+
# The official name of the canton in rhaeto-romanic.
|
26
|
+
def initialize(license_tag, name, name_de, name_fr, name_it, name_rt)
|
27
|
+
@license_tag = license_tag
|
28
|
+
@name = name
|
29
|
+
@names = {
|
30
|
+
:de => name_de,
|
31
|
+
:fr => name_fr,
|
32
|
+
:it => name_it,
|
33
|
+
:rt => name_rt,
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
# The name of the canton. If no language is passed, the local language is used.
|
38
|
+
#
|
39
|
+
# @param [Symbol, nil] language
|
40
|
+
# One of nil, :de, :fr, :it or :rt
|
41
|
+
def name(language=nil)
|
42
|
+
language ? @names[language] : @name
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Array<String>]
|
46
|
+
# The name of this zip code in all languages (only unique names)
|
47
|
+
def names
|
48
|
+
@names.values.uniq
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Hash]
|
52
|
+
# All properties of the canton as a hash.
|
53
|
+
def to_hash
|
54
|
+
{
|
55
|
+
:name => @name,
|
56
|
+
:name_de => @names[:de],
|
57
|
+
:name_fr => @names[:fr],
|
58
|
+
:name_it => @names[:it],
|
59
|
+
:name_rt => @names[:rt],
|
60
|
+
:license_tag => @license_tag
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
alias to_s name
|
65
|
+
|
66
|
+
# @private
|
67
|
+
# @see Object#hash
|
68
|
+
def hash
|
69
|
+
[self.class, @license_tag].hash
|
70
|
+
end
|
71
|
+
|
72
|
+
# @private
|
73
|
+
# @see Object#eql?
|
74
|
+
def eql?(other)
|
75
|
+
self.class == other.class && @license_tag == other.license_tag
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [String]
|
79
|
+
# @see Object#inspect
|
80
|
+
def inspect
|
81
|
+
sprintf "\#<%s:%014x %s>", self.class, object_id, self
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
require 'swissmatch/canton'
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
module SwissMatch
|
10
|
+
|
11
|
+
# Represents a collection of swiss cantons and provides a query interface.
|
12
|
+
class Cantons
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
# @param [Array<SwissMatch::Canton>] cantons
|
16
|
+
# The SwissMatch::Canton objects this SwissMatch::Cantons should contain
|
17
|
+
def initialize(cantons)
|
18
|
+
@cantons = cantons
|
19
|
+
@by_license_tag = {}
|
20
|
+
@by_name = {}
|
21
|
+
|
22
|
+
cantons.each do |canton|
|
23
|
+
@by_license_tag[canton.license_tag] = canton
|
24
|
+
canton.names.each do |name|
|
25
|
+
@by_name[name] = canton
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Calls the block once for every SwissMatch::Canton in this SwissMatch::Cantons
|
31
|
+
# instance, passing that canton as a parameter.
|
32
|
+
# The order is the same as the instance was constructed.
|
33
|
+
#
|
34
|
+
# @yield [canton]
|
35
|
+
# @yieldparam [SwissMatch::Canton] canton
|
36
|
+
#
|
37
|
+
# @return [self] Returns self
|
38
|
+
def each(&block)
|
39
|
+
@cantons.each(&block)
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# Calls the block once for every SwissMatch::Canton in this SwissMatch::Cantons
|
44
|
+
# instance, passing that canton as a parameter.
|
45
|
+
# The order is the reverse of what the instance was constructed.
|
46
|
+
#
|
47
|
+
# @yield [canton]
|
48
|
+
# @yieldparam [SwissMatch::Canton] canton
|
49
|
+
#
|
50
|
+
# @return [self] Returns self
|
51
|
+
def reverse_each(&block)
|
52
|
+
@cantons.reverse_each(&block)
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [SwissMatch::Cantons]
|
57
|
+
# A SwissMatch::Cantons collection with all SwissMatch::Canton objects for which the block
|
58
|
+
# returned true (or a trueish value)
|
59
|
+
def select(*args, &block)
|
60
|
+
Cantons.new(@cantons.select(*args, &block))
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [SwissMatch::Cantons]
|
64
|
+
# A SwissMatch::Cantons collection with all SwissMatch::Canton objects for which the block
|
65
|
+
# returned false (or a falseish value)
|
66
|
+
def reject(*args, &block)
|
67
|
+
Cantons.new(@cantons.reject(*args, &block))
|
68
|
+
end
|
69
|
+
|
70
|
+
# @see Enumerable#sort
|
71
|
+
#
|
72
|
+
# @return [SwissMatch::Cantons]
|
73
|
+
# A SwissMatch::Cantons collection sorted by the block
|
74
|
+
def sort(*args, &block)
|
75
|
+
Cantons.new(@cantons.sort(*args, &block))
|
76
|
+
end
|
77
|
+
|
78
|
+
# @see Enumerable#sort_by
|
79
|
+
#
|
80
|
+
# @return [SwissMatch::Cantons]
|
81
|
+
# A SwissMatch::Cantons collection sorted by the block
|
82
|
+
def sort_by(*args, &block)
|
83
|
+
Cantons.new(@cantons.sort_by(*args, &block))
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [SwissMatch::Canton]
|
87
|
+
# The canton with the given license tag or name (in any language)
|
88
|
+
def [](key)
|
89
|
+
@by_license_tag[key] || @by_name[key]
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [SwissMatch::Canton]
|
93
|
+
# The canton with the given license tag.
|
94
|
+
def by_license_tag(tag)
|
95
|
+
@by_license_tag[tag]
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [SwissMatch::Canton]
|
99
|
+
# The canton with the given name (any language)
|
100
|
+
def by_name(name)
|
101
|
+
@by_name[name]
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return [Integer] The number of SwissMatch::Canton objects in this collection.
|
105
|
+
def size
|
106
|
+
@cantons.size
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Array<SwissMatch::Canton>]
|
110
|
+
# An Array with all SwissMatch::Canton objects in this SwissMatch::Cantons.
|
111
|
+
def to_a
|
112
|
+
@cantons.dup
|
113
|
+
end
|
114
|
+
|
115
|
+
# @private
|
116
|
+
# @see Object#inspect
|
117
|
+
def inspect
|
118
|
+
sprintf "\#<%s:%x size: %d>", self.class, object_id>>1, size
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
module SwissMatch
|
6
|
+
|
7
|
+
# Represents a collection of SwissMatch::Community objects and provides a query
|
8
|
+
# interface.
|
9
|
+
class Communities
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
# @param [Array<SwissMatch::Community>] communities
|
13
|
+
# The SwissMatch::Community objects this SwissMatch::Communities should contain
|
14
|
+
def initialize(communities)
|
15
|
+
@communities = communities
|
16
|
+
reset!
|
17
|
+
end
|
18
|
+
|
19
|
+
# @private
|
20
|
+
# Reinitialize all caching instance variables
|
21
|
+
def reset!
|
22
|
+
@by_community_number = Hash[@communities.map { |c| [c.community_number, c] }]
|
23
|
+
@by_name = {}
|
24
|
+
@communities.each do |community|
|
25
|
+
@by_name[community.name.to_s] = community
|
26
|
+
end
|
27
|
+
|
28
|
+
unless @communities.size == @by_name.size
|
29
|
+
raise "ImplementationError: The author assumed communities to have a unique name, which doesn't seem to be the case anymore"
|
30
|
+
end
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# Calls the block once for every SwissMatch::Community in this SwissMatch::Communities
|
36
|
+
# instance, passing that community as a parameter.
|
37
|
+
# The order is the same as the instance was constructed.
|
38
|
+
#
|
39
|
+
# @yield [community]
|
40
|
+
# @yieldparam [SwissMatch::Community] community
|
41
|
+
#
|
42
|
+
# @return [self] Returns self
|
43
|
+
def each(&block)
|
44
|
+
@communities.each(&block)
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
# Calls the block once for every SwissMatch::Community in this SwissMatch::Communities
|
49
|
+
# instance, passing that community as a parameter.
|
50
|
+
# The order is the reverse of what the instance was constructed.
|
51
|
+
#
|
52
|
+
# @yield [community]
|
53
|
+
# @yieldparam [SwissMatch::Community] community
|
54
|
+
#
|
55
|
+
# @return [self] Returns self
|
56
|
+
def reverse_each(&block)
|
57
|
+
@communities.reverse_each(&block)
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [SwissMatch::Communities]
|
62
|
+
# A SwissMatch::Communities collection with all SwissMatch::Community objects for which the block
|
63
|
+
# returned true (or a trueish value)
|
64
|
+
def select(*args, &block)
|
65
|
+
Communities.new(@communities.select(*args, &block))
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [SwissMatch::Communities]
|
69
|
+
# A SwissMatch::Communities collection with all SwissMatch::Community objects for which the block
|
70
|
+
# returned false (or a falseish value)
|
71
|
+
def reject(*args, &block)
|
72
|
+
Communities.new(@communities.reject(*args, &block))
|
73
|
+
end
|
74
|
+
|
75
|
+
# @see Enumerable#sort
|
76
|
+
#
|
77
|
+
# @return [SwissMatch::Communities]
|
78
|
+
# A SwissMatch::Communities collection sorted by the block
|
79
|
+
def sort(*args, &block)
|
80
|
+
Communities.new(@communities.sort(*args, &block))
|
81
|
+
end
|
82
|
+
|
83
|
+
# @see Enumerable#sort_by
|
84
|
+
#
|
85
|
+
# @return [SwissMatch::Communities]
|
86
|
+
# A SwissMatch::Communities collection sorted by the block
|
87
|
+
def sort_by(*args, &block)
|
88
|
+
Communities.new(@communities.sort_by(*args, &block))
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [SwissMatch::Community]
|
92
|
+
# The community with the given name or community number.
|
93
|
+
def [](key)
|
94
|
+
@by_name[key] || @by_community_number[key.to_i]
|
95
|
+
end
|
96
|
+
|
97
|
+
# @return [SwissMatch::Community]
|
98
|
+
# The community with the given community number (also known as BFSNR).
|
99
|
+
def by_community_number(number)
|
100
|
+
@by_community_number[number]
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [SwissMatch::Community]
|
104
|
+
# The community with the given name.
|
105
|
+
def by_name(name)
|
106
|
+
@by_name[name]
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Integer] The number of SwissMatch::Community objects in this collection.
|
110
|
+
def size
|
111
|
+
@communities.size
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return [Array<SwissMatch::Community>]
|
115
|
+
# An Array with all SwissMatch::Community objects in this SwissMatch::Communities.
|
116
|
+
def to_a
|
117
|
+
@communities.dup
|
118
|
+
end
|
119
|
+
|
120
|
+
# @private
|
121
|
+
# @see Object#inspect
|
122
|
+
def inspect
|
123
|
+
sprintf "\#<%s:%x size: %d>", self.class, object_id>>1, size
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
module SwissMatch
|
6
|
+
|
7
|
+
# Represents a swiss community.
|
8
|
+
# Swiss communities are identified by their community number (BFSNR).
|
9
|
+
class Community
|
10
|
+
|
11
|
+
# @return [Integer]
|
12
|
+
# A unique, never recycled identification number.
|
13
|
+
# Also known as BFSNR.
|
14
|
+
attr_reader :community_number
|
15
|
+
|
16
|
+
# @return [String]
|
17
|
+
# The official name of the community.
|
18
|
+
attr_reader :name
|
19
|
+
|
20
|
+
# @return [SwissMatch::Canton]
|
21
|
+
# The canton this community belongs to.
|
22
|
+
attr_reader :canton
|
23
|
+
|
24
|
+
# @return [SwissMatch::Community]
|
25
|
+
# The community this community is considered to be an agglomeration of.
|
26
|
+
# Note that a main community will reference itself.
|
27
|
+
attr_reader :agglomeration
|
28
|
+
|
29
|
+
# @param [Integer] community_number
|
30
|
+
# The identification number of the community, also known as BFSNR.
|
31
|
+
# @param [String] name
|
32
|
+
# The official name of the community
|
33
|
+
# @param [SwissMatch::Canton] canton
|
34
|
+
# The canton this community belongs to
|
35
|
+
# @param [SwissMatch::Community] agglomeration
|
36
|
+
# The community this community is considered to be an agglomeration of.
|
37
|
+
# Note that a main community will reference itself.
|
38
|
+
def initialize(community_number, name, canton, agglomeration)
|
39
|
+
@community_number = community_number
|
40
|
+
@name = name
|
41
|
+
@canton = canton
|
42
|
+
@agglomeration = agglomeration == :self ? self : agglomeration
|
43
|
+
end
|
44
|
+
|
45
|
+
alias to_s name
|
46
|
+
|
47
|
+
# @param [Boolean] retain_references
|
48
|
+
# If set to false, :agglomeration will be set to the community_number and
|
49
|
+
# :canton to the canton's license_tag.
|
50
|
+
#
|
51
|
+
# @return [Hash]
|
52
|
+
# All properties of the community as a hash.
|
53
|
+
def to_hash(retain_references=false)
|
54
|
+
if retain_references then
|
55
|
+
canton = @canton
|
56
|
+
agglomeration = @agglomeration
|
57
|
+
else
|
58
|
+
canton = @canton && @canton.license_tag
|
59
|
+
agglomeration = @agglomeration && @agglomeration.community_number
|
60
|
+
end
|
61
|
+
|
62
|
+
{
|
63
|
+
:community_number => @community_number,
|
64
|
+
:name => @name,
|
65
|
+
:canton => canton,
|
66
|
+
:agglomeration => agglomeration,
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
# @private
|
71
|
+
# @see Object#hash
|
72
|
+
def hash
|
73
|
+
[self.class, @community_number].hash
|
74
|
+
end
|
75
|
+
|
76
|
+
# @private
|
77
|
+
# @see Object#eql?
|
78
|
+
def eql?(other)
|
79
|
+
self.class == other.class && @community_number == other.community_number
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [String]
|
83
|
+
# @see Object#inspect
|
84
|
+
def inspect
|
85
|
+
sprintf "\#<%s:%014x %s, BFSNR %d>", self.class, object_id, self, @community_number
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,333 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
require 'swissmatch/loaderror'
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
module SwissMatch
|
10
|
+
|
11
|
+
# Deals with retrieving and updating the files provided by the swiss postal service,
|
12
|
+
# and loading the data from them.
|
13
|
+
#
|
14
|
+
# @todo
|
15
|
+
# The current handling of the urls is not clean. I don't know yet how the urls will
|
16
|
+
# change over iterations.
|
17
|
+
class DataFiles
|
18
|
+
|
19
|
+
# Used to generate the regular expressions used to parse the data files.
|
20
|
+
# Generates a regular expression, that matches +size+ tab separated fields,
|
21
|
+
# delimited by \r\n.
|
22
|
+
# @private
|
23
|
+
def self.generate_expression(size)
|
24
|
+
/^#{Array.new(size) { '([^\t]*)' }.join('\t')}\r\n/
|
25
|
+
end
|
26
|
+
|
27
|
+
# Regular expressions used to parse the different files.
|
28
|
+
# @private
|
29
|
+
Expressions = {
|
30
|
+
:community => generate_expression(4),
|
31
|
+
:zip_2 => generate_expression(6),
|
32
|
+
:zip_1 => generate_expression(13),
|
33
|
+
}
|
34
|
+
|
35
|
+
# @private
|
36
|
+
# The URL of the plz_p1 file
|
37
|
+
URLZip1 = "https://match.post.ch/download?file=10001&tid=11&rol=0"
|
38
|
+
|
39
|
+
# @private
|
40
|
+
# The URL of the plz_p2 file
|
41
|
+
URLZip2 = "https://match.post.ch/download?file=10002&tid=14&rol=0"
|
42
|
+
|
43
|
+
# @private
|
44
|
+
# The URL of the plz_c file
|
45
|
+
URLCommunity = "https://match.post.ch/download?file=10003&tid=13&rol=0"
|
46
|
+
|
47
|
+
# @private
|
48
|
+
# An array of all urls
|
49
|
+
URLAll = [URLZip1, URLZip2, URLCommunity]
|
50
|
+
|
51
|
+
# The data of all cantons
|
52
|
+
# @private
|
53
|
+
CantonData = [
|
54
|
+
["AG", "Aargau", "Aargau", "Argovie", "Argovia", "Argovia"],
|
55
|
+
["AI", "Appenzell Innerrhoden", "Appenzell Innerrhoden", "Appenzell Rhodes-Intérieures", "Appenzello Interno", "Appenzell Dadens"],
|
56
|
+
["AR", "Appenzell Ausserrhoden", "Appenzell Ausserrhoden", "Appenzell Rhodes-Extérieures", "Appenzello Esterno", "Appenzell Dadora"],
|
57
|
+
["BE", "Bern", "Bern", "Berne", "Berna", "Berna"],
|
58
|
+
["BL", "Basel-Landschaft", "Basel-Landschaft", "Bâle-Campagne", "Basilea Campagna", "Basilea-Champagna"],
|
59
|
+
["BS", "Basel-Stadt", "Basel-Stadt", "Bâle-Ville", "Basilea Città", "Basilea-Citad"],
|
60
|
+
["FR", "Freiburg", "Fribourg", "Fribourg", "Friburgo", "Friburg"],
|
61
|
+
["GE", "Genève", "Genf", "Genève", "Ginevra", "Genevra"],
|
62
|
+
["GL", "Glarus", "Glarus", "Glaris", "Glarona", "Glaruna"],
|
63
|
+
["GR", "Graubünden", "Graubünden", "Grisons", "Grigioni", "Grischun"],
|
64
|
+
["JU", "Jura", "Jura", "Jura", "Giura", "Giura"],
|
65
|
+
["LU", "Luzern", "Luzern", "Lucerne", "Lucerna", "Lucerna"],
|
66
|
+
["NE", "Neuchâtel", "Neuenburg", "Neuchâtel", "Neuchâtel", "Neuchâtel"],
|
67
|
+
["NW", "Nidwalden", "Nidwalden", "Nidwald", "Nidvaldo", "Sutsilvania"],
|
68
|
+
["OW", "Obwalden", "Obwalden", "Obwald", "Obvaldo", "Sursilvania"],
|
69
|
+
["SG", "St. Gallen", "St. Gallen", "Saint-Gall", "San Gallo", "Son Gagl"],
|
70
|
+
["SH", "Schaffhausen", "Schaffhausen", "Schaffhouse", "Sciaffusa", "Schaffusa"],
|
71
|
+
["SO", "Solothurn", "Solothurn", "Soleure", "Soletta", "Soloturn"],
|
72
|
+
["SZ", "Schwyz", "Schwyz", "Schwytz", "Svitto", "Sviz"],
|
73
|
+
["TG", "Thurgau", "Thurgau", "Thurgovie", "Turgovia", "Turgovia"],
|
74
|
+
["TI", "Ticino", "Tessin", "Tessin", "Ticino", "Tessin"],
|
75
|
+
["UR", "Uri", "Uri", "Uri", "Uri", "Uri"],
|
76
|
+
["VD", "Vaud", "Waadt", "Vaud", "Vaud", "Vad"],
|
77
|
+
["VS", "Valais", "Wallis", "Valais", "Vallese", "Vallais"],
|
78
|
+
["ZG", "Zug", "Zug", "Zoug", "Zugo", "Zug"],
|
79
|
+
["ZH", "Zürich", "Zürich", "Zurich", "Zurigo", "Turitg"],
|
80
|
+
["FL", "Fürstentum Liechtenstein", "Fürstentum Liechtenstein", "Liechtenstein", "Liechtenstein", "Liechtenstein"],
|
81
|
+
["DE", "Deutschland", "Deutschland", "Allemagne", "Germania", "Germania"],
|
82
|
+
["IT", "Italien", "Italien", "Italie", "Italia", "Italia"],
|
83
|
+
]
|
84
|
+
|
85
|
+
# The directory in which the post mat[ch] files reside
|
86
|
+
attr_accessor :data_directory
|
87
|
+
|
88
|
+
# @return [SwissMatch::Cantons] The loaded swiss cantons
|
89
|
+
attr_reader :cantons
|
90
|
+
|
91
|
+
# @return [SwissMatch::Communities] The loaded swiss communities
|
92
|
+
attr_reader :communities
|
93
|
+
|
94
|
+
# @return [SwissMatch::ZipCodes] The loaded swiss zip codes
|
95
|
+
attr_reader :zip_codes
|
96
|
+
|
97
|
+
# @return [Array<LoadError>] Errors that occurred while loading the data
|
98
|
+
attr_reader :errors
|
99
|
+
|
100
|
+
# @param [nil, String] data_directory
|
101
|
+
# The directory in which the post mat[ch] files reside
|
102
|
+
def initialize(data_directory=nil)
|
103
|
+
reset_errors!
|
104
|
+
if data_directory then
|
105
|
+
@data_directory = data_directory
|
106
|
+
elsif ENV['SWISSMATCH_DATA'] then
|
107
|
+
@data_directory = ENV['SWISSMATCH_DATA']
|
108
|
+
else
|
109
|
+
data_directory = File.expand_path('../../../data/swissmatch', __FILE__)
|
110
|
+
data_directory = Gem.datadir 'swissmatch' if defined?(Gem) && !File.directory?(data_directory)
|
111
|
+
@data_directory = data_directory
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Resets the list of errors that were encountered during load
|
116
|
+
# @return [self]
|
117
|
+
def reset_errors!
|
118
|
+
@errors = []
|
119
|
+
self
|
120
|
+
end
|
121
|
+
|
122
|
+
# Load new files
|
123
|
+
#
|
124
|
+
# @return [Array<String>]
|
125
|
+
# An array with the absolute file paths of the extracted files.
|
126
|
+
def load_updates
|
127
|
+
URLAll.flat_map { |url|
|
128
|
+
http_get_zip_file(url, @data_directory)
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
# Performs an HTTP-GET for the given url, extracts it as a zipped file into the
|
133
|
+
# destination directory.
|
134
|
+
#
|
135
|
+
# @return [Array<String>]
|
136
|
+
# An array with the absolute file paths of the extracted files.
|
137
|
+
def http_get_zip_file(url, destination)
|
138
|
+
require 'open-uri'
|
139
|
+
require 'swissmatch/zip' # patched zip/zip
|
140
|
+
require 'fileutils'
|
141
|
+
|
142
|
+
files = []
|
143
|
+
|
144
|
+
open(url) do |zip_buffer|
|
145
|
+
Zip::ZipFile.open(zip_buffer) do |zip_file|
|
146
|
+
zip_file.each do |f|
|
147
|
+
target_path = File.join(destination, f.name)
|
148
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
149
|
+
zip_file.extract(f, target_path) unless File.exist?(target_path)
|
150
|
+
files << target_path
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
files
|
156
|
+
end
|
157
|
+
|
158
|
+
# Unzips it as a zipped file into the destination directory.
|
159
|
+
def unzip_file(file, destination)
|
160
|
+
require 'swissmatch/zip'
|
161
|
+
Zip::ZipFile.open(file) do |zip_file|
|
162
|
+
zip_file.each do |f|
|
163
|
+
target_path = File.join(destination, f.name)
|
164
|
+
FileUtils.mkdir_p(File.dirname(target_path))
|
165
|
+
zip_file.extract(f, target_path) unless File.exist?(target_path)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Used to convert numerical language codes to symbols
|
171
|
+
LanguageCodes = [nil, :de, :fr, :it, :rt]
|
172
|
+
|
173
|
+
# Loads the data into this DataFiles instance
|
174
|
+
#
|
175
|
+
# @return [self]
|
176
|
+
# Returns self.
|
177
|
+
def load!
|
178
|
+
@cantons, @communities, @zip_codes = *load
|
179
|
+
self
|
180
|
+
end
|
181
|
+
|
182
|
+
# @return [Array]
|
183
|
+
# Returns an array of the form [SwissMatch::Cantons, SwissMatch::Communities,
|
184
|
+
# SwissMatch::ZipCodes].
|
185
|
+
def load
|
186
|
+
reset_errors!
|
187
|
+
|
188
|
+
cantons = load_cantons
|
189
|
+
communities = load_communities(cantons)
|
190
|
+
zip_codes = load_zipcodes(cantons, communities)
|
191
|
+
|
192
|
+
[cantons, communities, zip_codes]
|
193
|
+
end
|
194
|
+
|
195
|
+
# @return [SwissMatch::Cantons]
|
196
|
+
# A SwissMatch::Cantons containing all cantons used by the swiss postal service.
|
197
|
+
def load_cantons
|
198
|
+
Cantons.new(
|
199
|
+
CantonData.map { |tag, name, name_de, name_fr, name_it, name_rt|
|
200
|
+
Canton.new(tag, name, name_de, name_fr, name_it, name_rt)
|
201
|
+
}
|
202
|
+
)
|
203
|
+
end
|
204
|
+
|
205
|
+
# @return [SwissMatch::Communities]
|
206
|
+
# An instance of SwissMatch::Communities containing all communities defined by the
|
207
|
+
# files known to this DataFiles instance.
|
208
|
+
def load_communities(cantons)
|
209
|
+
raise "Must load cantons first" unless cantons
|
210
|
+
|
211
|
+
file = Dir.enum_for(:glob, "#{@data_directory}/plz_c_*.txt").last
|
212
|
+
temporary = []
|
213
|
+
complete = {}
|
214
|
+
load_table(file, :community).each do |bfsnr, name, canton, agglomeration|
|
215
|
+
bfsnr = bfsnr.to_i
|
216
|
+
agglomeration = agglomeration.to_i
|
217
|
+
canton = cantons.by_license_tag(canton)
|
218
|
+
if agglomeration == bfsnr then
|
219
|
+
complete[bfsnr] = Community.new(bfsnr, name, canton, :self)
|
220
|
+
elsif agglomeration.nil? then
|
221
|
+
complete[bfsnr] = Community.new(bfsnr, name, canton, nil)
|
222
|
+
else
|
223
|
+
temporary << [bfsnr, name, canton, agglomeration]
|
224
|
+
end
|
225
|
+
end
|
226
|
+
temporary.each do |bfsnr, name, canton, agglomeration|
|
227
|
+
community = complete[agglomeration]
|
228
|
+
raise "Incomplete community referenced by #{bfsnr}: #{agglomeration}" unless agglomeration
|
229
|
+
complete[bfsnr] = Community.new(bfsnr, name, canton, community)
|
230
|
+
end
|
231
|
+
|
232
|
+
Communities.new(complete.values)
|
233
|
+
end
|
234
|
+
|
235
|
+
# TODO: load all files, not just the most recent
|
236
|
+
# TODO: calculate valid_until dates
|
237
|
+
#
|
238
|
+
# @return [SwissMatch::ZipCodes]
|
239
|
+
# An instance of SwissMatch::ZipCodes containing all zip codes defined by the
|
240
|
+
# files known to this DataFiles instance.
|
241
|
+
def load_zipcodes(cantons, communities)
|
242
|
+
raise "Must load cantons first" unless cantons
|
243
|
+
raise "Must load communities first" unless communities
|
244
|
+
|
245
|
+
temporary = {}
|
246
|
+
self_delivered = []
|
247
|
+
others = []
|
248
|
+
zip1_file = Dir.enum_for(:glob, "#{@data_directory}/plz_p1_*.txt").last
|
249
|
+
zip2_file = Dir.enum_for(:glob, "#{@data_directory}/plz_p2_*.txt").last
|
250
|
+
load_table(zip1_file, :zip_1).each do |row|
|
251
|
+
onrp = row.at(0).to_i
|
252
|
+
delivery_by = row.at(10).to_i
|
253
|
+
delivery_by = case delivery_by when 0 then nil; when onrp then :self; else delivery_by; end
|
254
|
+
language = LanguageCodes[row.at(7).to_i]
|
255
|
+
language_alternative = LanguageCodes[row.at(8).to_i]
|
256
|
+
name_short = Name.new(row.at(4), language)
|
257
|
+
name = Name.new(row.at(5), language)
|
258
|
+
data = [
|
259
|
+
onrp, # ordering_number
|
260
|
+
row.at(1).to_i, # type
|
261
|
+
row.at(2).to_i, # code
|
262
|
+
row.at(3).to_i, # add_on
|
263
|
+
name, # name (official)
|
264
|
+
[name], # names (official + alternative)
|
265
|
+
name_short, # name_short (official)
|
266
|
+
[name_short], # names_short (official + alternative)
|
267
|
+
[], # PLZ2 type 3 short names (additional region names)
|
268
|
+
[], # PLZ2 type 3 names (additional region names)
|
269
|
+
cantons.by_license_tag(row.at(6)), # canton
|
270
|
+
language,
|
271
|
+
language_alternative,
|
272
|
+
row.at(9) == "1", # sortfile_member
|
273
|
+
delivery_by, # delivery_by
|
274
|
+
communities.by_community_number(row.at(11).to_i), # community_number
|
275
|
+
Date.civil(*row.at(12).match(/^(\d{4})(\d\d)(\d\d)$/).captures.map(&:to_i)) # valid_from
|
276
|
+
]
|
277
|
+
temporary[onrp] = data
|
278
|
+
if :self == delivery_by then
|
279
|
+
self_delivered << data
|
280
|
+
else
|
281
|
+
others << data
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
load_table(zip2_file, :zip_2).each do |onrp, rn, type, lang, short, name|
|
286
|
+
onrp = onrp.to_i
|
287
|
+
lang_code = lang.to_i
|
288
|
+
language = LanguageCodes[lang_code]
|
289
|
+
entry = temporary[onrp]
|
290
|
+
if type == "2"
|
291
|
+
entry[5] << Name.new(name, language, rn.to_i)
|
292
|
+
entry[7] << Name.new(short, language, rn.to_i)
|
293
|
+
elsif type == "3"
|
294
|
+
entry[8] << Name.new(name, language, rn.to_i)
|
295
|
+
entry[9] << Name.new(short, language, rn.to_i)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
self_delivered.each do |row|
|
300
|
+
temporary[row.at(0)] = ZipCode.new(*row)
|
301
|
+
end
|
302
|
+
others.each do |row|
|
303
|
+
if row.at(14) then
|
304
|
+
raise "Delivery not found:\n#{row.inspect}" unless tmp = temporary[row.at(14)]
|
305
|
+
if tmp.kind_of?(Array) then
|
306
|
+
@errors << LoadError.new("Invalid reference: onrp #{row.at(0)} delivery by #{row.at(14)}", row)
|
307
|
+
row[14] = nil
|
308
|
+
else
|
309
|
+
row[14] = tmp
|
310
|
+
end
|
311
|
+
end
|
312
|
+
temporary[row.at(0)] = ZipCode.new(*row)
|
313
|
+
end
|
314
|
+
|
315
|
+
ZipCodes.new(temporary.values)
|
316
|
+
end
|
317
|
+
|
318
|
+
# Reads a file and parses using the pattern of the given name.
|
319
|
+
#
|
320
|
+
# @param [String] path
|
321
|
+
# The path of the file to parse
|
322
|
+
# @param [Symbol] pattern
|
323
|
+
# The pattern-name used to parse the file (see Expressions)
|
324
|
+
#
|
325
|
+
# @return [Array<Array<String>>]
|
326
|
+
# A 2 dimensional array representing the tabular data contained in the given file.
|
327
|
+
def load_table(path, pattern)
|
328
|
+
File.read(path, :encoding => Encoding::Windows_1252.to_s). # to_s because sadly, ruby 1.9.2 can't handle an Encoding instance as argument
|
329
|
+
encode(Encoding::UTF_8).
|
330
|
+
scan(Expressions[pattern])
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|