itax_code 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,97 @@
1
+ module ItaxCode
2
+ ##
3
+ # This class handles the tax code generation logic.
4
+ #
5
+ # @param [String] surname The citizen first name
6
+ # @param [String] name The citizen last name
7
+ # @param [String] gender The citizen gender
8
+ # @param [String, Date] birthdate The citizen birthdate
9
+ # @param [String] birthplace The citizen birthplace
10
+ #
11
+ # @example
12
+ #
13
+ # ItaxCode::Encoder.new(
14
+ # surname: "Rossi",
15
+ # name: "Matteo",
16
+ # gender: "M",
17
+ # birthdate: "1990-08-23",
18
+ # birthplace: "Milano"
19
+ # ).encode
20
+ #
21
+ # @return [String] The encoded tax code
22
+
23
+ class Encoder
24
+ def initialize(data = {}, utils = Utils.new)
25
+ @surname = data[:surname]
26
+ @name = data[:name]
27
+ @gender = data[:gender].try :upcase
28
+ @birthdate = parsed_date data[:birthdate]
29
+ @birthplace = data[:birthplace]
30
+ @utils = utils
31
+ end
32
+
33
+ ##
34
+ # This method calculates the tax code.
35
+ #
36
+ # @return [String] The calculated tax code
37
+
38
+ def encode
39
+ code = encode_surname
40
+ code += encode_name
41
+ code += encode_birthdate
42
+ code += encode_birthplace
43
+ code += utils.encode_cin code
44
+ code
45
+ end
46
+
47
+ private
48
+
49
+ attr_accessor :surname,
50
+ :name,
51
+ :gender,
52
+ :birthdate,
53
+ :birthplace,
54
+ :utils
55
+
56
+ def encode_surname
57
+ str = utils.slugged(surname).chars
58
+ consonants = utils.extract_consonants str
59
+ vowels = utils.extract_vowels str
60
+ "#{consonants[0..2]}#{vowels[0..2]}XXX"[0..2].upcase
61
+ end
62
+
63
+ def encode_name
64
+ str = utils.slugged(name).chars
65
+ consonants = utils.extract_consonants str
66
+ vowels = utils.extract_vowels str
67
+
68
+ if consonants.length > 3
69
+ arr = consonants.dup.chars
70
+ arr.delete_at 1
71
+ consonants = arr.join
72
+ end
73
+
74
+ "#{consonants[0..2]}#{vowels[0..2]}XXX"[0..2].upcase
75
+ end
76
+
77
+ def encode_birthdate
78
+ year = birthdate.year.to_s[2..-1]
79
+ month = utils.months[birthdate.month - 1]
80
+ day = format "%02d", (birthdate.day + (gender == "F" ? 40 : 0))
81
+ "#{year}#{month}#{day}"
82
+ end
83
+
84
+ def encode_birthplace
85
+ utils.municipalities.find do |m|
86
+ utils.slugged(m["name"]) == utils.slugged(birthplace)
87
+ end.try(:[], "code")
88
+ end
89
+
90
+ def parsed_date(date)
91
+ case date.class.name
92
+ when "Date", "Time", "DateTime" then date
93
+ else Date.parse(date)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,114 @@
1
+ module ItaxCode
2
+ ##
3
+ # This class handles the parsing logic for TaxCode module.
4
+ #
5
+ # @param [String] tax_code
6
+ #
7
+ # @example
8
+ #
9
+ # ItaxCode::Parser.new("RSSMRA70A01L726S").decode
10
+ #
11
+ # @return [Hash]
12
+
13
+ class Parser
14
+ def initialize(tax_code, utils = Utils.new)
15
+ @tax_code = (tax_code || "").upcase
16
+ @utils = utils
17
+ end
18
+
19
+ ##
20
+ # This method decodes the tax code.
21
+ #
22
+ # @return [Hash]
23
+
24
+ def decode
25
+ year = decode_year
26
+ month = utils.months.find_index(raw[:birthdate_month]) + 1
27
+ day, gender = decode_day_and_gender
28
+ birthplace = decode_birthplace
29
+
30
+ {
31
+ code: tax_code,
32
+ gender: gender,
33
+ birthdate: [year, month, day].join("-"),
34
+ birthplace: birthplace,
35
+ omocodes: omocodes,
36
+ raw: raw
37
+ }
38
+ end
39
+
40
+ private
41
+
42
+ attr_accessor :tax_code, :utils
43
+
44
+ def raw
45
+ matches = tax_code.scan(utils.regex).flatten
46
+
47
+ {
48
+ surname: matches[0],
49
+ name: matches[1],
50
+ birthdate: matches[2],
51
+ birthdate_year: matches[3],
52
+ birthdate_month: matches[4],
53
+ birthdate_day: matches[5],
54
+ birthplace: matches[6],
55
+ cin: matches[7]
56
+ }
57
+ end
58
+
59
+ def decode_year
60
+ year = utils.omocodia_decode raw[:birthdate_year]
61
+ year = (Date.today.year.to_s[0..1] + year).to_i
62
+ year -= 100 if year > Date.today.year
63
+ year
64
+ end
65
+
66
+ def decode_day_and_gender
67
+ day = utils.omocodia_decode(raw[:birthdate_day]).to_i
68
+
69
+ if day > 40
70
+ day -= 40
71
+ [day, "F"]
72
+ else
73
+ [day, "M"]
74
+ end
75
+ end
76
+
77
+ def decode_birthplace
78
+ places = utils.municipalities
79
+ .select { |m| m["code"] == municipality_code }
80
+ place = places.find { |m| !m["name"].include? "soppresso" }
81
+ place = (place.presence || places.last).deep_symbolize_keys
82
+ place[:name] = place[:name].gsub(" (soppresso)", "")
83
+ place
84
+ end
85
+
86
+ def municipality_code
87
+ raw[:birthplace][0] + utils.omocodia_decode(raw[:birthplace][1..-1])
88
+ end
89
+
90
+ def omocodes
91
+ code_chars = tax_code[0..14].chars
92
+ codes = []
93
+
94
+ utils.omocodia_subs_indexes.reverse_each do |i|
95
+ code_chars[i] = utils.omocodia_decode(code_chars[i])
96
+ end
97
+
98
+ code = code_chars.join
99
+ code_cin = utils.encode_cin(code)
100
+ code += code_cin
101
+ codes.push(code)
102
+
103
+ utils.omocodia_subs_indexes.reverse_each do |i|
104
+ code_chars[i] = utils.omocodia_encode(code_chars[i])
105
+ code = code_chars.join
106
+ code_cin = utils.encode_cin(code)
107
+ code += code_cin
108
+ codes.push(code)
109
+ end
110
+
111
+ codes
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,112 @@
1
+ module ItaxCode
2
+ class Utils
3
+ def regex
4
+ /^([A-Z]{3})([A-Z]{3})
5
+ (([A-Z\d]{2})([ABCDEHLMPRST]{1})([A-Z\d]{2}))
6
+ ([A-Z]{1}[A-Z\d]{3})
7
+ ([A-Z]{1})$/x
8
+ end
9
+
10
+ def slugged(str, separator = "-")
11
+ str.gsub!(/\s*@\s*/, " at ")
12
+ str.gsub!(/\s*&\s*/, " and ")
13
+ str.parameterize(separator: separator)
14
+ end
15
+
16
+ def months
17
+ %w[A B C D E H L M P R S T]
18
+ end
19
+
20
+ def consonants
21
+ %w[b c d f g h j k l m n p q r s t v w x y z]
22
+ end
23
+
24
+ def vowels
25
+ %w[a e i o u]
26
+ end
27
+
28
+ def cin_odds
29
+ {
30
+ "0": 1, "1": 0, "2": 5, "3": 7, "4": 9, "5": 13,
31
+ "6": 15, "7": 17, "8": 19, "9": 21, "A": 1, "B": 0,
32
+ "C": 5, "D": 7, "E": 9, "F": 13, "G": 15, "H": 17,
33
+ "I": 19, "J": 21, "K": 2, "L": 4, "M": 18, "N": 20,
34
+ "O": 11, "P": 3, "Q": 6, "R": 8, "S": 12, "T": 14,
35
+ "U": 16, "V": 10, "W": 22, "X": 25, "Y": 24, "Z": 23
36
+ }
37
+ end
38
+
39
+ def cin_evens
40
+ {
41
+ "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5,
42
+ "6": 6, "7": 7, "8": 8, "9": 9, "A": 0, "B": 1,
43
+ "C": 2, "D": 3, "E": 4, "F": 5, "G": 6, "H": 7,
44
+ "I": 8, "J": 9, "K": 10, "L": 11, "M": 12, "N": 13,
45
+ "O": 14, "P": 15, "Q": 16, "R": 17, "S": 18, "T": 19,
46
+ "U": 20, "V": 21, "W": 22, "X": 23, "Y": 24, "Z": 25
47
+ }
48
+ end
49
+
50
+ def cin_remainders
51
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
52
+ end
53
+
54
+ def omocodia
55
+ {
56
+ "0": "L", "1": "M", "2": "N", "3": "P", "4": "Q",
57
+ "5": "R", "6": "S", "7": "T", "8": "U", "9": "V"
58
+ }
59
+ end
60
+
61
+ def omocodia_digits
62
+ omocodia.keys.join
63
+ end
64
+
65
+ def omocodia_letters
66
+ omocodia.values.join
67
+ end
68
+
69
+ def omocodia_subs_indexes
70
+ [6, 7, 9, 10, 12, 13, 14]
71
+ end
72
+
73
+ def omocodia_encode(val)
74
+ val.tr omocodia_digits, omocodia_letters
75
+ end
76
+
77
+ def omocodia_decode(val)
78
+ val.tr omocodia_letters, omocodia_digits
79
+ end
80
+
81
+ def extract_consonants(str)
82
+ str.select { |c| consonants.include? c }.join
83
+ end
84
+
85
+ def extract_vowels(str)
86
+ str.select { |c| vowels.include? c }.join
87
+ end
88
+
89
+ def encode_cin(code)
90
+ tot = 0
91
+
92
+ code.chars.each_with_index do |char, index|
93
+ tot += cin_odds[char.to_sym] if (index + 1).odd?
94
+ tot += cin_evens[char.to_sym] if (index + 1).even?
95
+ end
96
+
97
+ cin_remainders[tot % 26]
98
+ end
99
+
100
+ def municipalities
101
+ @municipalities ||= JSON.parse(
102
+ File.read("#{__dir__}/data/municipalities.json")
103
+ )
104
+ end
105
+
106
+ # def countries
107
+ # @countries ||= JSON.parse(
108
+ # File.read("#{__dir__}/data/countries.json")
109
+ # )
110
+ # end
111
+ end
112
+ end
@@ -0,0 +1,44 @@
1
+ module ItaxCode
2
+ ##
3
+ # This class holds the TaxCode validation logic.
4
+ #
5
+ # @param [Hash] data The citizen input data
6
+
7
+ class Validator
8
+ LENGTH = 16
9
+
10
+ def initialize(data = {})
11
+ @encoded = Encoder.new(data).encode
12
+ end
13
+
14
+ class << self
15
+ ##
16
+ # This method checks tax code standard length
17
+ # against citizen and business fical code standards.
18
+ #
19
+ # @param [String] code The tax code
20
+ #
21
+ # @return [true, false]
22
+
23
+ def standard_length?(code)
24
+ code.length == LENGTH
25
+ end
26
+ end
27
+
28
+ ##
29
+ # This method check pre calculated tax code validity
30
+ # against newly encoded tax code.
31
+ #
32
+ # @param [String] tax_code The pre calculated tax code
33
+ #
34
+ # @return [true, false]
35
+
36
+ def valid?(tax_code)
37
+ encoded[0..10] == tax_code[0..10]
38
+ end
39
+
40
+ private
41
+
42
+ attr_accessor :encoded
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module ItaxCode
2
+ VERSION = "0.1.2".freeze
3
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: itax_code
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Matteo Rossi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-11-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mocha
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-performance
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description:
140
+ email:
141
+ - mttrss5@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".gitignore"
147
+ - ".rubocop.yml"
148
+ - ".travis.yml"
149
+ - CHANGELOG.md
150
+ - CODE_OF_CONDUCT.md
151
+ - Gemfile
152
+ - Gemfile.lock
153
+ - README.md
154
+ - Rakefile
155
+ - bin/console
156
+ - bin/setup
157
+ - itax_code.gemspec
158
+ - lib/itax_code.rb
159
+ - lib/itax_code/data/countries.json
160
+ - lib/itax_code/data/municipalities.json
161
+ - lib/itax_code/encoder.rb
162
+ - lib/itax_code/parser.rb
163
+ - lib/itax_code/utils.rb
164
+ - lib/itax_code/validator.rb
165
+ - lib/itax_code/version.rb
166
+ homepage: https://github.com/matteoredz/itax-code
167
+ licenses:
168
+ - MIT
169
+ metadata:
170
+ homepage_uri: https://github.com/matteoredz/itax-code
171
+ source_code_uri: https://github.com/matteoredz/itax-code
172
+ changelog_uri: https://github.com/matteoredz/itax-code/CHANGELOG.md
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - ">="
180
+ - !ruby/object:Gem::Version
181
+ version: 2.5.0
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubygems_version: 3.1.2
189
+ signing_key:
190
+ specification_version: 4
191
+ summary: Encode and decode Italian tax code (Codice Fiscale).
192
+ test_files: []