tin_valid 0.1.0 → 1.0.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.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +37 -1
- data/README.md +93 -15
- data/lib/tin_valid/austria_tin.rb +19 -6
- data/lib/tin_valid/germany_tin.rb +69 -0
- data/lib/tin_valid/greece_tin.rb +9 -0
- data/lib/tin_valid/hungary_tin.rb +23 -0
- data/lib/tin_valid/ireland_tin.rb +50 -0
- data/lib/tin_valid/italy_tin.rb +170 -0
- data/lib/tin_valid/latvia_tin.rb +59 -0
- data/lib/tin_valid/lithuania_tin.rb +72 -0
- data/lib/tin_valid/luxembourg_tin.rb +107 -0
- data/lib/tin_valid/malta_tin.rb +45 -0
- data/lib/tin_valid/netherlands_tin.rb +29 -0
- data/lib/tin_valid/poland_tin.rb +78 -0
- data/lib/tin_valid/portugal_tin.rb +38 -0
- data/lib/tin_valid/romania_tin.rb +84 -0
- data/lib/tin_valid/slovakia_tin.rb +64 -0
- data/lib/tin_valid/slovenia_tin.rb +32 -0
- data/lib/tin_valid/spain_tin.rb +45 -0
- data/lib/tin_valid/united_kingdom_tin.rb +22 -0
- data/lib/tin_valid/version.rb +1 -1
- data/lib/tin_valid.rb +18 -4
- metadata +19 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c64b5917083db71c567a2b80c3edf35d58d32b07da94b9e0ced0e189a3bb4f13
|
4
|
+
data.tar.gz: 9785af41b29fc7c335d1f7f062ed8ae217a421e145005a783bd56eeb43d75c5a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '097ef7fcbdeb7f95b0456fdcd930501f9da4eb85ddb9bc0a5a8e9996d58aa03939fe409d2a0ed7f0c25ae1bee786bcce19a3c079e91969b62839337d581f5d3a'
|
7
|
+
data.tar.gz: e4bb19d3238b04228390f78e505375af8e695f2cb3060c518f6aa6e41b2b61db2f065fc483ade6a84f45213e05c90f55f76b4a18f36f54b1a0615f505bf08e05
|
data/.rspec
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,41 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.0.0] - 2025-04-17
|
4
|
+
|
5
|
+
Features:
|
6
|
+
- Add Italy 🇮🇹
|
7
|
+
- Add Latvia 🇱🇻
|
8
|
+
- Add Luxembourg 🇱🇺
|
9
|
+
- Add Lithuania 🇱🇹
|
10
|
+
- Add Malta 🇲🇹
|
11
|
+
- Add Netherlands 🇳🇱
|
12
|
+
- Add Poland 🇵🇱
|
13
|
+
- Add Portugal 🇵🇹
|
14
|
+
- Add Romania 🇷🇴
|
15
|
+
- Add Slovakia 🇸🇰
|
16
|
+
- Add Slovenia 🇸🇮
|
17
|
+
- Add Spain 🇪🇸
|
18
|
+
- Add United Kingdom 🇬🇧
|
19
|
+
|
20
|
+
## [0.1.1] - 2025-04-15
|
21
|
+
|
22
|
+
Features:
|
23
|
+
- Add Germany 🇩🇪
|
24
|
+
- Add Greece 🇬🇷
|
25
|
+
- Add Hungary 🇭🇺
|
26
|
+
- Add Ireland 🇮🇪
|
27
|
+
|
3
28
|
## [0.1.0] - 2025-04-14
|
4
29
|
|
5
|
-
- Initial release
|
30
|
+
- Initial release with:
|
31
|
+
- Austria 🇦🇹
|
32
|
+
- Belgium 🇧🇪
|
33
|
+
- Bulgaria 🇧🇬
|
34
|
+
- Croatia 🇭🇷
|
35
|
+
- Cyprus 🇨🇾
|
36
|
+
- Czechia 🇨🇿
|
37
|
+
- Denmark 🇩🇰
|
38
|
+
- Estonia 🇪🇪
|
39
|
+
- Germany 🇩🇪
|
40
|
+
- Greece 🇬🇷
|
41
|
+
- Sweden 🇸🇪
|
data/README.md
CHANGED
@@ -1,20 +1,40 @@
|
|
1
|
-
# TinValid
|
1
|
+
# TinValid 🇪🇺
|
2
2
|
|
3
3
|
Validate Tax Identification Numbers (TINs) for the following European countries:
|
4
4
|
|
5
|
-
- Austria
|
6
|
-
- Belgium
|
7
|
-
- Bulgaria
|
8
|
-
- Croatia
|
9
|
-
- Cyprus
|
10
|
-
- Czechia
|
11
|
-
- Denmark
|
12
|
-
- Estonia
|
13
|
-
-
|
5
|
+
- Austria 🇦🇹
|
6
|
+
- Belgium 🇧🇪
|
7
|
+
- Bulgaria 🇧🇬
|
8
|
+
- Croatia 🇭🇷
|
9
|
+
- Cyprus 🇨🇾
|
10
|
+
- Czechia 🇨🇿
|
11
|
+
- Denmark 🇩🇰
|
12
|
+
- Estonia 🇪🇪
|
13
|
+
- Germany 🇩🇪
|
14
|
+
- Greece 🇬🇷
|
15
|
+
- Hungary 🇭🇺
|
16
|
+
- Ireland 🇮🇪
|
17
|
+
- Italy 🇮🇹
|
18
|
+
- Latvia 🇱🇻
|
19
|
+
- Lithuania 🇱🇹
|
20
|
+
- Luxembourg 🇱🇺
|
21
|
+
- Malta 🇲🇹
|
22
|
+
- Netherlands 🇳🇱
|
23
|
+
- Poland 🇵🇱
|
24
|
+
- Portugal 🇵🇹
|
25
|
+
- Romania 🇷🇴
|
26
|
+
- Slovakia 🇸🇰
|
27
|
+
- Slovenia 🇸🇮
|
28
|
+
- Spain 🇪🇸
|
29
|
+
- Sweden 🇸🇪
|
30
|
+
- United Kingdom 🇬🇧
|
31
|
+
|
32
|
+
See also the [descriptions of the structure provided by the European
|
33
|
+
Union](https://taxation-customs.ec.europa.eu/online-services/online-services-and-databases-taxation/taxpayer-identification-number-tin_en).
|
14
34
|
|
15
35
|
## Installation
|
16
36
|
|
17
|
-
|
37
|
+
Add the gem to your application’s Gemfile by executing:
|
18
38
|
|
19
39
|
```bash
|
20
40
|
bundle add tin_valid
|
@@ -31,7 +51,7 @@ gem install tin_valid
|
|
31
51
|
|
32
52
|
```rb
|
33
53
|
# Austria
|
34
|
-
TinValid::AustriaTin.new(tin: "…").valid?
|
54
|
+
TinValid::AustriaTin.new(tin: "…").valid? # => true
|
35
55
|
|
36
56
|
# Belgium
|
37
57
|
# Optional birth_date
|
@@ -60,9 +80,67 @@ TinValid::DenmarkTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
|
60
80
|
# Optional birth_date
|
61
81
|
TinValid::EstoniaTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
62
82
|
|
83
|
+
# Germany
|
84
|
+
TinValid::GermanyTin.new(tin: "…").valid?
|
85
|
+
|
86
|
+
# Greece
|
87
|
+
TinValid::GreeceTin.new(tin: "…").valid?
|
88
|
+
|
89
|
+
# Hungary
|
90
|
+
TinValid::HungaryTin.new(tin: "…").valid?
|
91
|
+
|
92
|
+
# Ireland
|
93
|
+
TinValid::IrelandTin.new(tin: "…").valid?
|
94
|
+
|
95
|
+
# Italy
|
96
|
+
# Optional birth_date
|
97
|
+
TinValid::ItalyTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
98
|
+
|
99
|
+
# Latvia
|
100
|
+
# Optional birth_date
|
101
|
+
TinValid::LatviaTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
102
|
+
|
103
|
+
# Lithuania
|
104
|
+
# Optional birth_date
|
105
|
+
TinValid::LithuaniaTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
106
|
+
|
107
|
+
# Luxembourg
|
108
|
+
# Optional birth_date
|
109
|
+
TinValid::LuxembourgTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
110
|
+
|
111
|
+
# Malta
|
112
|
+
TinValid::MaltaTin.new(tin: "…").valid?
|
113
|
+
|
114
|
+
# Netherlands
|
115
|
+
TinValid::NetherlandsTin.new(tin: "…").valid?
|
116
|
+
|
117
|
+
# Poland
|
118
|
+
# Optional birth_date
|
119
|
+
TinValid::PolandTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
120
|
+
|
121
|
+
# Portugal
|
122
|
+
TinValid::PortugalTin.new(tin: "…").valid?
|
123
|
+
|
124
|
+
# Romania
|
125
|
+
# Optional birth_date
|
126
|
+
TinValid::RomaniaTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
127
|
+
|
128
|
+
# Slovakia
|
129
|
+
# Optional birth_date
|
130
|
+
TinValid::SlovakiaTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
131
|
+
|
132
|
+
# Slovenia
|
133
|
+
TinValid::SloveniaTin.new(tin: "…").valid?
|
134
|
+
|
135
|
+
# Spain
|
136
|
+
TinValid::SpainTin.new(tin: "…").valid?
|
137
|
+
|
63
138
|
# Sweden
|
64
139
|
# Optional birth_date
|
65
140
|
TinValid::SwedenTin.new(tin: "…", birth_date: Date.new(…)).valid?
|
141
|
+
|
142
|
+
# United Kingdom
|
143
|
+
TinValid::UnitedKingdomTin.new(tin: "…").valid?
|
66
144
|
```
|
67
145
|
|
68
146
|
## Development
|
@@ -80,10 +158,10 @@ git commits and the created tag, and push the `.gem` file to
|
|
80
158
|
## Contributing
|
81
159
|
|
82
160
|
Bug reports and pull requests are welcome on GitHub at
|
83
|
-
https://github.com/
|
161
|
+
https://github.com/cults/tin_valid. This project is intended to be a safe,
|
84
162
|
welcoming space for collaboration, and contributors are expected to adhere to
|
85
163
|
the
|
86
|
-
[code of conduct](https://github.com/
|
164
|
+
[code of conduct](https://github.com/cults/tin_valid/blob/main/CODE_OF_CONDUCT.md).
|
87
165
|
|
88
166
|
## License
|
89
167
|
|
@@ -94,4 +172,4 @@ The gem is available as open source under the terms of the
|
|
94
172
|
|
95
173
|
Everyone interacting in the TinValid project's codebases, issue trackers, chat
|
96
174
|
rooms and mailing lists is expected to follow the
|
97
|
-
[code of conduct](https://github.com/
|
175
|
+
[code of conduct](https://github.com/cults/tin_valid/blob/main/CODE_OF_CONDUCT.md).
|
@@ -1,15 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module TinValid
|
4
|
-
class AustriaTin
|
4
|
+
class AustriaTin
|
5
|
+
def initialize(tin:)
|
6
|
+
@tin = tin
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :tin
|
10
|
+
|
5
11
|
def valid?
|
6
|
-
return false unless
|
12
|
+
return false unless MATCHER.match?(normalized)
|
7
13
|
|
8
|
-
|
14
|
+
normalized[-1] == check
|
15
|
+
end
|
16
|
+
|
17
|
+
def normalized
|
18
|
+
@normalized ||= tin.to_s.strip.tr("-/", "")
|
9
19
|
end
|
10
20
|
|
11
21
|
private
|
12
22
|
|
23
|
+
MATCHER = %r{\A[0-9]{2}-?[0-9]{3}/?[0-9]{4}\z}
|
24
|
+
private_constant :MATCHER
|
25
|
+
|
13
26
|
# rubocop:disable Metrics/AbcSize
|
14
27
|
def check
|
15
28
|
# 1. Multiply the values of each position by the corresponding weight:
|
@@ -22,10 +35,10 @@ module TinValid
|
|
22
35
|
# - C7: 1
|
23
36
|
# - C8: 2
|
24
37
|
values_by_weight =
|
25
|
-
|
26
|
-
.chars
|
38
|
+
normalized
|
39
|
+
.chars[..-2]
|
27
40
|
.each_with_index
|
28
|
-
.map { |n, i| n.to_i * (i.even? ?
|
41
|
+
.map { |n, i| n.to_i * (i.even? ? 1 : 2) }
|
29
42
|
|
30
43
|
# 2. If the product of a doubling operation is > 9, sum the digits of the
|
31
44
|
# product;
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class GermanyTin < Data.define(:tin)
|
5
|
+
def valid?
|
6
|
+
valid_v1? || valid_v2?
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def valid_v1?
|
12
|
+
# 1. 13 characters
|
13
|
+
# 2. Only digits
|
14
|
+
# 3. C5 is a 0
|
15
|
+
/\A[0-9]{4}0[0-9]{8}\z/.match?(tin)
|
16
|
+
end
|
17
|
+
|
18
|
+
# rubocop:disable Metrics/AbcSize
|
19
|
+
def valid_v2?
|
20
|
+
# C1: Must never be 0.
|
21
|
+
return false unless /\A[1-9][0-9]{10}\z/.match?(tin)
|
22
|
+
|
23
|
+
# C1-C10: One and only one mandatory duplicate or triple value:
|
24
|
+
# - One of the first ten digits is used twice (the recurrent digits do
|
25
|
+
# not have to be located at subsequent positions but they can be);
|
26
|
+
# - One of the first ten digits is used tree times (only two recurrent
|
27
|
+
# digits are allowed to be one after another).
|
28
|
+
consecutive_chars =
|
29
|
+
tin[..-2]
|
30
|
+
.chars
|
31
|
+
.group_by(&:itself)
|
32
|
+
.select { |_, chars| chars.size > 1 }
|
33
|
+
|
34
|
+
return false if consecutive_chars.size != 1
|
35
|
+
return false if consecutive_chars.first.last.size > 3
|
36
|
+
|
37
|
+
tin[-1].to_i == check_v2
|
38
|
+
end
|
39
|
+
# rubocop:enable Metrics/AbcSize
|
40
|
+
|
41
|
+
def check_v2
|
42
|
+
# 1. Initialize the variable X to 10.
|
43
|
+
y = tin[..-2].chars.inject(10) do |x, char|
|
44
|
+
# 2. Take C1 + X modulo 10. If result is 0, result is 10;
|
45
|
+
result = (char.to_i + x) % 10
|
46
|
+
result = 10 if result == 0
|
47
|
+
|
48
|
+
# 3. Multiply the result by 2;
|
49
|
+
result *= 2
|
50
|
+
|
51
|
+
# 4. Take modulo 11 of the result. Update the value of variable X with
|
52
|
+
# the result of this operation;
|
53
|
+
x = result % 11
|
54
|
+
|
55
|
+
# 5. Take C2 + X modulo 10. If result is 0, result is 10;
|
56
|
+
# 6. Multiply the result by 2;
|
57
|
+
# 7. Take modulo 11 of the result. Update the value of variable X with
|
58
|
+
# the result of this operation;
|
59
|
+
# 8. Apply steps 5, 6 and 7 in an analogue way for digits C3 to C10.
|
60
|
+
# Consider that last value called Y;
|
61
|
+
x
|
62
|
+
end
|
63
|
+
|
64
|
+
# 9. 11 - Y = check digit. If check digit = 10, replace it by 0.
|
65
|
+
check = 11 - y
|
66
|
+
check == 10 ? 0 : check
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class HungaryTin < Data.define(:tin)
|
5
|
+
def valid?
|
6
|
+
return false unless /\A8[0-9]{9}\z/.match?(tin)
|
7
|
+
|
8
|
+
tin[-1].to_i == check
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def check
|
14
|
+
# 1. Multiply the values of each position by the corresponding weight:
|
15
|
+
# 2. Add up the results of the above multiplications;
|
16
|
+
result = (1..9).each_with_index.sum { |num, i| num * tin[i].to_i }
|
17
|
+
|
18
|
+
# 3. Get modulo 11 of the result of the previous addition;
|
19
|
+
# 4. Check digit = remainder.
|
20
|
+
result % 11
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class IrelandTin < Data.define(:tin)
|
5
|
+
def valid?
|
6
|
+
/\A[0-9]{7}[A-W][A-IW]?\z/.match?(tin) &&
|
7
|
+
tin.chars[7] == check
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
# rubocop:disable Metrics/AbcSize
|
13
|
+
def check
|
14
|
+
# 1. In reverse order, each digit is multiplied by a weight started at 2:
|
15
|
+
weights = 8.downto(2)
|
16
|
+
digits = weights.with_index.map { |weight, i| weight * tin.chars[i].to_i }
|
17
|
+
|
18
|
+
# 2. LetterToNumber(C9) is based on the following mapping:
|
19
|
+
# “A”=1, “B”=2, “C”=3, “D”=4, “E”=5, “F”=6, “G”=7, “H”=8, “I”=9
|
20
|
+
# A “W” or absence of character in position 9 is allocated a numeric
|
21
|
+
# value of 0.
|
22
|
+
digits.push letter_to_number(tin.chars[9 - 1]) * 9
|
23
|
+
|
24
|
+
# 3. Add up each result;
|
25
|
+
sum = digits.sum
|
26
|
+
|
27
|
+
# 4. The remainder of the modulo 23 indicates the character position on
|
28
|
+
# the alphabet according to the following mapping:
|
29
|
+
# 0=”W”, 1=”A”, 2=”B”, 3=”C”… 22=”V”
|
30
|
+
number_to_letter(sum % 23)
|
31
|
+
end
|
32
|
+
# rubocop:enable Metrics/AbcSize
|
33
|
+
|
34
|
+
def letter_to_number(letter)
|
35
|
+
return 0 if letter.nil?
|
36
|
+
|
37
|
+
case letter
|
38
|
+
in "W" then 0
|
39
|
+
in "A".."I" then letter.ord - 64
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def number_to_letter(number)
|
44
|
+
case number
|
45
|
+
in 0 then "W"
|
46
|
+
in 1..22 then (number + 64).chr
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
# rubocop:disable Metrics/ClassLength
|
5
|
+
class ItalyTin
|
6
|
+
def initialize(tin:, birth_date: nil)
|
7
|
+
@tin = tin
|
8
|
+
@birth_date = birth_date
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :tin, :birth_date
|
12
|
+
|
13
|
+
def valid?
|
14
|
+
match = MATCHER.match(tin)
|
15
|
+
return false unless match
|
16
|
+
return false unless valid_date?(match)
|
17
|
+
|
18
|
+
tin[-1] == check
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def valid_date?(match)
|
24
|
+
# C7,C8: Two last digits of a year.
|
25
|
+
year = replacement_to_i(match[:year])
|
26
|
+
|
27
|
+
# C9: A letter representing a month;
|
28
|
+
month = MONTH_LETTERS.fetch(match[:month])
|
29
|
+
|
30
|
+
# C10,C11: Day of month (in the range 1...31 for men)
|
31
|
+
# or day of month + 40 (in the range 41...71 for women).
|
32
|
+
day = replacement_to_i(match[:day])
|
33
|
+
day -= 40 if day > 40
|
34
|
+
|
35
|
+
if birth_date
|
36
|
+
birth_date == date("#{birth_century}#{year}", month, day)
|
37
|
+
else
|
38
|
+
date?("19#{year}", month, day) || date?("20#{year}", month, day)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def check
|
43
|
+
# 1. Each of the first fifteen characters,depending on its relevant
|
44
|
+
# position (even or odd), is converted into a numeric value,according to
|
45
|
+
# correspondence shown in the tables below:
|
46
|
+
sum = tin[..14].chars.each.with_index(1).sum do |char, index|
|
47
|
+
index.even? ? even_number(char) : odd_number(char)
|
48
|
+
end
|
49
|
+
|
50
|
+
# 2. The numerical values thus determined are added together and their sum
|
51
|
+
# is divided by 26. The check character (C16) is obtained by converting
|
52
|
+
# the remainder of the division in the corresponding alphabetic character
|
53
|
+
# according to the table below:
|
54
|
+
((sum % 26) + 65).chr
|
55
|
+
end
|
56
|
+
|
57
|
+
def birth_century = birth_date.strftime("%Y")[..1]
|
58
|
+
|
59
|
+
def replacement_to_i(string)
|
60
|
+
string.chars.map { NUMERICAL_REPLACEMENTS.fetch(_1, _1) }.join.to_i
|
61
|
+
end
|
62
|
+
|
63
|
+
def date?(year, month, day)
|
64
|
+
found_date = date(year, month, day)
|
65
|
+
found_date && found_date < Date.today
|
66
|
+
end
|
67
|
+
|
68
|
+
def date(year, month, day)
|
69
|
+
Date.new(year.to_i, month.to_i, day.to_i)
|
70
|
+
rescue Date::Error
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
MATCHER = %r{
|
75
|
+
\A
|
76
|
+
[A-Z]{6}
|
77
|
+
(?<year>[0-9LMNPQRSTUV]{2})
|
78
|
+
(?<month>[ABCDEHLMPRST])
|
79
|
+
(?<day>[0-7LMNPQRST][0-9LMNPQRSTUV])
|
80
|
+
[A-Z]
|
81
|
+
[0-9LMNPQRSTUV]{3}
|
82
|
+
[A-Z]
|
83
|
+
\z
|
84
|
+
}x
|
85
|
+
private_constant :MATCHER
|
86
|
+
|
87
|
+
# C9: A letter representing a month; the letter can only take the values:
|
88
|
+
MONTH_LETTERS = {
|
89
|
+
"A" => 1,
|
90
|
+
"B" => 2,
|
91
|
+
"C" => 3,
|
92
|
+
"D" => 4,
|
93
|
+
"E" => 5,
|
94
|
+
"H" => 6,
|
95
|
+
"L" => 7,
|
96
|
+
"M" => 8,
|
97
|
+
"P" => 9,
|
98
|
+
"R" => 10,
|
99
|
+
"S" => 11,
|
100
|
+
"T" => 12
|
101
|
+
}.freeze
|
102
|
+
private_constant :MONTH_LETTERS
|
103
|
+
|
104
|
+
NUMERICAL_REPLACEMENTS = {
|
105
|
+
"L" => 0,
|
106
|
+
"M" => 1,
|
107
|
+
"N" => 2,
|
108
|
+
"P" => 3,
|
109
|
+
"Q" => 4,
|
110
|
+
"R" => 5,
|
111
|
+
"S" => 6,
|
112
|
+
"T" => 7,
|
113
|
+
"U" => 8,
|
114
|
+
"V" => 9
|
115
|
+
}.freeze
|
116
|
+
private_constant :NUMERICAL_REPLACEMENTS
|
117
|
+
|
118
|
+
def even_number(character)
|
119
|
+
case character
|
120
|
+
in "0".."9" then character.to_i
|
121
|
+
in "A".."Z" then character.ord - 65
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def odd_number(character)
|
126
|
+
ODD_NUMBER_TABLE.fetch(character)
|
127
|
+
end
|
128
|
+
|
129
|
+
ODD_NUMBER_TABLE = {
|
130
|
+
"A" => 1,
|
131
|
+
"0" => 1,
|
132
|
+
"B" => 0,
|
133
|
+
"1" => 0,
|
134
|
+
"C" => 5,
|
135
|
+
"2" => 5,
|
136
|
+
"D" => 7,
|
137
|
+
"3" => 7,
|
138
|
+
"E" => 9,
|
139
|
+
"4" => 9,
|
140
|
+
"F" => 13,
|
141
|
+
"5" => 13,
|
142
|
+
"G" => 15,
|
143
|
+
"6" => 15,
|
144
|
+
"H" => 17,
|
145
|
+
"7" => 17,
|
146
|
+
"I" => 19,
|
147
|
+
"8" => 19,
|
148
|
+
"J" => 21,
|
149
|
+
"9" => 21,
|
150
|
+
"K" => 2,
|
151
|
+
"L" => 4,
|
152
|
+
"M" => 18,
|
153
|
+
"N" => 20,
|
154
|
+
"O" => 11,
|
155
|
+
"P" => 3,
|
156
|
+
"Q" => 6,
|
157
|
+
"R" => 8,
|
158
|
+
"S" => 12,
|
159
|
+
"T" => 14,
|
160
|
+
"U" => 16,
|
161
|
+
"V" => 10,
|
162
|
+
"W" => 22,
|
163
|
+
"X" => 25,
|
164
|
+
"Y" => 24,
|
165
|
+
"Z" => 23
|
166
|
+
}.freeze
|
167
|
+
private_constant :ODD_NUMBER_TABLE
|
168
|
+
end
|
169
|
+
# rubocop:enable Metrics/ClassLength
|
170
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class LatviaTin < Data.define(:tin, :birth_date)
|
5
|
+
def initialize(tin:, birth_date: nil)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid? = valid_v1? || valid_v2?
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
MATCHER_V1 = %r{
|
14
|
+
\A
|
15
|
+
(?<day>[0-3][0-9])
|
16
|
+
(?<month>[0-1][0-9])
|
17
|
+
(?<year>[0-9]{2})
|
18
|
+
(?<century>[0-2])
|
19
|
+
-?
|
20
|
+
[0-9]{4}
|
21
|
+
\z
|
22
|
+
}x
|
23
|
+
private_constant :MATCHER_V1
|
24
|
+
|
25
|
+
MATCHER_V2 = %r{
|
26
|
+
\A
|
27
|
+
32
|
28
|
+
[0-9]{4}
|
29
|
+
-?
|
30
|
+
[0-9]{5}
|
31
|
+
\z
|
32
|
+
}x
|
33
|
+
private_constant :MATCHER_V2
|
34
|
+
|
35
|
+
def valid_v1?
|
36
|
+
match = MATCHER_V1.match(tin)
|
37
|
+
return false unless match
|
38
|
+
|
39
|
+
if birth_date
|
40
|
+
tin[..5] == birth_date.strftime("%d%m%y") &&
|
41
|
+
tin[6] == birth_century_digit
|
42
|
+
else
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid_v2?
|
48
|
+
MATCHER_V2.match?(tin)
|
49
|
+
end
|
50
|
+
|
51
|
+
def birth_century_digit
|
52
|
+
case birth_date.year
|
53
|
+
when 1800..1899 then "0"
|
54
|
+
when 1900..1999 then "1"
|
55
|
+
when 2000.. then "2"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class LithuaniaTin < Data.define(:tin, :birth_date)
|
5
|
+
def initialize(tin:, birth_date: nil)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
matcher = MATCHER.match(tin)
|
11
|
+
return false unless matcher
|
12
|
+
|
13
|
+
if birth_date && (matcher[:birth_date] != birth_date.strftime("%y%m%d"))
|
14
|
+
return false
|
15
|
+
end
|
16
|
+
|
17
|
+
tin[-1].to_i == check
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
MATCHER = %r{
|
23
|
+
[1-6]
|
24
|
+
(?<birth_date>
|
25
|
+
[0-9]{2}
|
26
|
+
[0-1][0-9]
|
27
|
+
[0-3][0-9]
|
28
|
+
)
|
29
|
+
[0-9]{4}
|
30
|
+
}x
|
31
|
+
private_constant :MATCHER
|
32
|
+
|
33
|
+
# rubocop:disable Metrics/AbcSize
|
34
|
+
# rubocop:disable Metrics/MethodLength
|
35
|
+
def check
|
36
|
+
# 1. Multiply the values of each position by the corresponding weight:
|
37
|
+
weights =
|
38
|
+
[1, 2, 3, 4, 5, 6, 7, 8, 9, 1]
|
39
|
+
.each_with_index
|
40
|
+
.map { |weight, index| weight * tin[index].to_i }
|
41
|
+
|
42
|
+
# 2. Add up the results of the above multiplications;
|
43
|
+
sum = weights.sum
|
44
|
+
|
45
|
+
# 3. Get modulo 11 of the result of the previous addition;
|
46
|
+
remainder = sum % 11
|
47
|
+
|
48
|
+
# 4. C11 = remainder if remainder is not 10;
|
49
|
+
return remainder if remainder != 10
|
50
|
+
|
51
|
+
# 5. If remainder is 10, calculate a new check digit with over
|
52
|
+
# corresponding weight:
|
53
|
+
weights =
|
54
|
+
[3, 4, 5, 6, 7, 8, 9, 1, 2, 3]
|
55
|
+
.each_with_index
|
56
|
+
.map { |weight, index| weight * tin[index].to_i }
|
57
|
+
|
58
|
+
# 6. Add up the results of the above multiplications;
|
59
|
+
sum = weights.sum
|
60
|
+
|
61
|
+
# 7. Get modulo 11 of the result of the previous addition;
|
62
|
+
remainder = sum % 11
|
63
|
+
|
64
|
+
# 8. C11 = remainder if remainder is not 10; if remainder is 10, C11 = 0.
|
65
|
+
return remainder if remainder != 10
|
66
|
+
|
67
|
+
0
|
68
|
+
end
|
69
|
+
# rubocop:enable Metrics/AbcSize
|
70
|
+
# rubocop:enable Metrics/MethodLength
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class LuxembourgTin < Data.define(:tin, :birth_date)
|
5
|
+
def initialize(tin:, birth_date: nil)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
match = MATCHER.match(tin)
|
11
|
+
return false unless match
|
12
|
+
|
13
|
+
tin_date = date(match[:year], match[:month], match[:day])
|
14
|
+
return false unless tin_date
|
15
|
+
|
16
|
+
return false if birth_date && birth_date != tin_date
|
17
|
+
|
18
|
+
check1? && check2?
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
MATCHER = %r{
|
24
|
+
\A
|
25
|
+
(?<year>[0-9]{4})
|
26
|
+
(?<month>[0-1][0-9])
|
27
|
+
(?<day>[0-3][0-9])
|
28
|
+
[0-9]{5}
|
29
|
+
\z
|
30
|
+
}x
|
31
|
+
private_constant :MATCHER
|
32
|
+
|
33
|
+
def check1?
|
34
|
+
# 1. Multiply the values of each position by the corresponding weight:
|
35
|
+
weights =
|
36
|
+
[2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
|
37
|
+
.each_with_index
|
38
|
+
.map { |weight, index| weight * tin[index].to_i }
|
39
|
+
|
40
|
+
# 2. If the product of a doubling operation is > 9, sum the digits of the
|
41
|
+
# product;
|
42
|
+
weights = weights.map { _1 > 9 ? _1.to_s.chars.sum(&:to_i) : _1 }
|
43
|
+
|
44
|
+
# 3. Add up the results of the above multiplications;
|
45
|
+
sum = weights.sum
|
46
|
+
|
47
|
+
# 4. Get modulo 10 of the result of the previous addition;
|
48
|
+
remainder = sum % 10
|
49
|
+
|
50
|
+
# 5. If remainder = 0, C12 is valid. Otherwise the TIN is not valid.
|
51
|
+
remainder == 0
|
52
|
+
end
|
53
|
+
|
54
|
+
def check2?
|
55
|
+
# 1. Create an array n containing the individual C1 to C11 and C13 of the
|
56
|
+
# TIN (where ni = the value of the corresponding C), taken from right to
|
57
|
+
# left:
|
58
|
+
array = [*tin[..10].chars, tin[12]].map(&:to_i).reverse
|
59
|
+
|
60
|
+
# 2. Initialize the checksum c to 0;
|
61
|
+
# 3. For each index i of the array n, starting at 0, replace c by
|
62
|
+
# d(c,p(i mod 8, ni)), according to the following tables:
|
63
|
+
checksum = array.each_with_index.inject(0) do |c, (ni, index)|
|
64
|
+
table_d(c, table_p(index % 8, ni))
|
65
|
+
end
|
66
|
+
|
67
|
+
# 4. Check digit c if c = 0, C13 is valid. Otherwise, the TIN is not
|
68
|
+
# valid.
|
69
|
+
checksum == 0
|
70
|
+
end
|
71
|
+
|
72
|
+
def date(year, month, day)
|
73
|
+
Date.new(year.to_i, month.to_i, day.to_i)
|
74
|
+
rescue Date::Error
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def table_d(number_i, number_j) = TABLE_D.dig(number_i, number_j)
|
79
|
+
def table_p(number_m, number_n) = TABLE_P.dig(number_m, number_n)
|
80
|
+
|
81
|
+
TABLE_D = [
|
82
|
+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
83
|
+
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
|
84
|
+
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
|
85
|
+
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
|
86
|
+
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
|
87
|
+
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
|
88
|
+
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
|
89
|
+
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
|
90
|
+
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
|
91
|
+
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
|
92
|
+
].freeze
|
93
|
+
private_constant :TABLE_D
|
94
|
+
|
95
|
+
TABLE_P = [
|
96
|
+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
97
|
+
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
|
98
|
+
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
|
99
|
+
[8, 9, 1, 6, 0, 5, 3, 5, 2, 7],
|
100
|
+
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
|
101
|
+
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
|
102
|
+
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
|
103
|
+
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
|
104
|
+
].freeze
|
105
|
+
private_constant :TABLE_P
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class MaltaTin < Data.define(:tin)
|
5
|
+
def valid? = valid_format_1? || valid_format_2?
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
MATCHER_V1 = %r{
|
10
|
+
\A
|
11
|
+
(?<number>
|
12
|
+
(?<part1>[0-9]{0,5})
|
13
|
+
[0-9]{1,2}
|
14
|
+
)
|
15
|
+
(?<letter>[MGAPLHBZ])
|
16
|
+
\z
|
17
|
+
}x
|
18
|
+
private_constant :MATCHER_V1
|
19
|
+
|
20
|
+
MATCHER_V2 = %r{
|
21
|
+
\A
|
22
|
+
(11|22|33|44|55|66|77|88)
|
23
|
+
[0-9]{7}
|
24
|
+
\z
|
25
|
+
}x
|
26
|
+
private_constant :MATCHER_V2
|
27
|
+
|
28
|
+
def valid_format_1?
|
29
|
+
match = MATCHER_V1.match(tin)
|
30
|
+
return false unless match
|
31
|
+
|
32
|
+
case match[:letter]
|
33
|
+
when "A", "P"
|
34
|
+
true
|
35
|
+
when "M", "G", "L", "H", "B", "Z"
|
36
|
+
(0..32_000).cover?(match[:part1].to_i) &&
|
37
|
+
match[:number].to_i != 0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid_format_2?
|
42
|
+
MATCHER_V2.match?(tin)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class NetherlandsTin < Data.define(:tin)
|
5
|
+
def valid?
|
6
|
+
return false unless /\A[0-9]{9}\z/.match?(tin)
|
7
|
+
|
8
|
+
tin[-1].to_i == check
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def check
|
14
|
+
# 1. Multiply the values of each position by the corresponding weight:
|
15
|
+
multipliers =
|
16
|
+
9
|
17
|
+
.downto(2)
|
18
|
+
.each_with_index
|
19
|
+
.map { |multiplier, position| multiplier * tin[position].to_i }
|
20
|
+
|
21
|
+
# 2. Add up the results of the above multiplications;
|
22
|
+
sum = multipliers.sum
|
23
|
+
|
24
|
+
# 3. Get modulo 11 of the result of the previous addition;
|
25
|
+
# 4. Check digit = remainder (if remainder = 10, the TIN is not valid).
|
26
|
+
sum % 11
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class PolandTin < Data.define(:tin, :birth_date)
|
5
|
+
def initialize(tin:, birth_date: nil)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid? = valid_v1? || valid_v2?
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
MATCHER_V1 = /\A[0-9]{10}\z/
|
14
|
+
private_constant :MATCHER_V1
|
15
|
+
|
16
|
+
MATCHER_V2 = %r{
|
17
|
+
\A
|
18
|
+
(?<year>[0-9]{2})
|
19
|
+
(?<month>[0-9]{2})
|
20
|
+
(?<day>[0-3][0-9])
|
21
|
+
[0-9]{5}
|
22
|
+
\z
|
23
|
+
}x
|
24
|
+
private_constant :MATCHER_V2
|
25
|
+
|
26
|
+
def valid_v1?
|
27
|
+
return false unless MATCHER_V1.match?(tin)
|
28
|
+
|
29
|
+
tin[-1].to_i == check
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid_v2?
|
33
|
+
match = MATCHER_V2.match(tin)
|
34
|
+
return false unless match
|
35
|
+
return true unless birth_date
|
36
|
+
|
37
|
+
tin_date = date(
|
38
|
+
"#{birth_century}#{match[:year]}",
|
39
|
+
match[:month].to_i - month_increase,
|
40
|
+
match[:day],
|
41
|
+
)
|
42
|
+
tin_date == birth_date
|
43
|
+
end
|
44
|
+
|
45
|
+
def check
|
46
|
+
# 1. Multiply the values of each position by the corresponding weight:
|
47
|
+
weights =
|
48
|
+
[6, 5, 7, 2, 3, 4, 5, 6, 7]
|
49
|
+
.each_with_index
|
50
|
+
.map { |weight, index| weight * tin[index].to_i }
|
51
|
+
|
52
|
+
# 2. Add up the results of the above multiplications;
|
53
|
+
sum = weights.sum
|
54
|
+
|
55
|
+
# 3. Get modulo 11 of the result of the previous addition;
|
56
|
+
# 4. Check digit = remainder (if remainder = 10, the TIN is not valid).
|
57
|
+
sum % 11
|
58
|
+
end
|
59
|
+
|
60
|
+
def birth_century = birth_date.year.to_s[..1].to_i
|
61
|
+
|
62
|
+
def month_increase
|
63
|
+
case birth_century
|
64
|
+
when 18 then 80
|
65
|
+
when 20 then 20
|
66
|
+
when 21 then 40
|
67
|
+
when 22 then 60
|
68
|
+
else 0
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def date(year, month, day)
|
73
|
+
Date.new(year.to_i, month.to_i, day.to_i)
|
74
|
+
rescue Date::Error
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class PortugalTin < Data.define(:tin)
|
5
|
+
def valid?
|
6
|
+
return false unless /\A[0-9]{9}\z/.match?(tin)
|
7
|
+
|
8
|
+
tin[-1].to_i == check
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def check
|
14
|
+
# 1. Multiply the values of each position by the corresponding weight:
|
15
|
+
multipliers =
|
16
|
+
9
|
17
|
+
.downto(2)
|
18
|
+
.with_index
|
19
|
+
.map { |multiplier, index| multiplier * tin[index].to_i }
|
20
|
+
|
21
|
+
# 2. Add up the results of the above multiplications;
|
22
|
+
sum = multipliers.sum
|
23
|
+
|
24
|
+
# 3. Get modulo 11 of the result of the previous addition;
|
25
|
+
remainder = sum % 11
|
26
|
+
|
27
|
+
# 4. Check digit = 11 - remainder:
|
28
|
+
digit = 11 - remainder
|
29
|
+
|
30
|
+
# If check digit < = 9 then check digit is OK (11 – remainder);
|
31
|
+
return digit if digit <= 9
|
32
|
+
|
33
|
+
# If check digit = 10 then check digit is 0 (zero);
|
34
|
+
# If check digit = 11 then check digit is 0 (zero).
|
35
|
+
0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class RomaniaTin < Data.define(:tin, :birth_date)
|
5
|
+
def initialize(tin:, birth_date: nil)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid? = valid_v1? || valid_v2?
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
MATCHER_V1 = %r{
|
14
|
+
\A
|
15
|
+
(?<century_code>[1-9])
|
16
|
+
(?<year>[0-9]{2})
|
17
|
+
(?<month>[0-1][0-9])
|
18
|
+
(?<day>[0-3][0-9])
|
19
|
+
(?<district>[0-5][0-9])
|
20
|
+
[0-9]{4}
|
21
|
+
\z
|
22
|
+
}x
|
23
|
+
private_constant :MATCHER_V1
|
24
|
+
|
25
|
+
MATCHER_V2 = %r{
|
26
|
+
\A
|
27
|
+
9
|
28
|
+
000
|
29
|
+
[0-9]{8,9}
|
30
|
+
\z
|
31
|
+
}x
|
32
|
+
private_constant :MATCHER_V2
|
33
|
+
|
34
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
35
|
+
# rubocop:disable Metrics/MethodLength
|
36
|
+
def valid_v1?
|
37
|
+
match = MATCHER_V1.match(tin)
|
38
|
+
return false unless match
|
39
|
+
return false unless valid_district?(match[:district])
|
40
|
+
|
41
|
+
century = tin_century_from_code(match[:century_code]) || birth_century
|
42
|
+
if century
|
43
|
+
tin_date = date(
|
44
|
+
"#{century}#{match[:year]}",
|
45
|
+
match[:month],
|
46
|
+
match[:day],
|
47
|
+
)
|
48
|
+
return false if tin_date.nil?
|
49
|
+
return false if birth_date && tin_date != birth_date
|
50
|
+
end
|
51
|
+
|
52
|
+
true
|
53
|
+
end
|
54
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
55
|
+
# rubocop:enable Metrics/MethodLength
|
56
|
+
|
57
|
+
def valid_v2? = MATCHER_V2.match?(tin)
|
58
|
+
|
59
|
+
def valid_district?(district)
|
60
|
+
case district.to_i
|
61
|
+
when 1..47, 51..52 then true
|
62
|
+
else false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def birth_century
|
67
|
+
birth_date.year.to_s[..1] if birth_date
|
68
|
+
end
|
69
|
+
|
70
|
+
def tin_century_from_code(code)
|
71
|
+
case code.to_i
|
72
|
+
when 1..2 then 19
|
73
|
+
when 3..4 then 18
|
74
|
+
when 5..6 then 20
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def date(year, month, day)
|
79
|
+
Date.new(year.to_i, month.to_i, day.to_i)
|
80
|
+
rescue Date::Error
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class SlovakiaTin < Data.define(:tin, :birth_date)
|
5
|
+
def initialize(tin:, birth_date: nil)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid? = valid_v2? || valid_v1?
|
10
|
+
|
11
|
+
def normalized = tin.tr("/", "")
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
MATCHER_V1 = %r{
|
16
|
+
\A
|
17
|
+
(?<year>[0-9]{2})
|
18
|
+
(?<month>[0-6][0-9])
|
19
|
+
(?<day>[0-3][0-9])
|
20
|
+
/?
|
21
|
+
[0-9]{3}
|
22
|
+
(?<check>[0-9])?
|
23
|
+
\z
|
24
|
+
}x
|
25
|
+
private_constant :MATCHER_V1
|
26
|
+
|
27
|
+
MATCHER_V2 = %r{
|
28
|
+
\A
|
29
|
+
[0-9]{10}
|
30
|
+
\z
|
31
|
+
}x
|
32
|
+
private_constant :MATCHER_V2
|
33
|
+
|
34
|
+
# rubocop:disable Metrics/MethodLength
|
35
|
+
def valid_v1?
|
36
|
+
match = MATCHER_V1.match(tin)
|
37
|
+
return false unless match
|
38
|
+
|
39
|
+
year = match[:year].to_i
|
40
|
+
return false if year >= 54 && match[:check].nil?
|
41
|
+
|
42
|
+
if birth_date
|
43
|
+
month = match[:month].to_i
|
44
|
+
month -= 50 if month > 50
|
45
|
+
|
46
|
+
tin_date = date("#{birth_century}#{year}", month, match[:day])
|
47
|
+
return false if tin_date != birth_date
|
48
|
+
end
|
49
|
+
|
50
|
+
true
|
51
|
+
end
|
52
|
+
# rubocop:enable Metrics/MethodLength
|
53
|
+
|
54
|
+
def valid_v2? = MATCHER_V2.match?(tin)
|
55
|
+
|
56
|
+
def birth_century = birth_date.year.to_s[..1]
|
57
|
+
|
58
|
+
def date(year, month, day)
|
59
|
+
Date.new(year.to_i, month.to_i, day.to_i)
|
60
|
+
rescue Date::Error
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class SloveniaTin < Data.define(:tin)
|
5
|
+
def valid?
|
6
|
+
return false unless /\A[1-9][0-9]{7}\z/.match?(tin)
|
7
|
+
|
8
|
+
tin[-1].to_i == check
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def check
|
14
|
+
# Multiply the values of each position by the corresponding weight:
|
15
|
+
weights =
|
16
|
+
8
|
17
|
+
.downto(2)
|
18
|
+
.each_with_index
|
19
|
+
.map { |weight, index| weight * tin[index].to_i }
|
20
|
+
|
21
|
+
# 2. Add up the results of the above multiplications;
|
22
|
+
sum = weights.sum
|
23
|
+
|
24
|
+
# 3. Get modulo 11 of the result of the previous addition;
|
25
|
+
remainder = sum % 11
|
26
|
+
|
27
|
+
# 4. Check digit = 11 - remainder. If result = 10, Check digit = 0.
|
28
|
+
digit = 11 - remainder
|
29
|
+
digit == 10 ? 0 : digit
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class SpainTin < Data.define(:tin)
|
5
|
+
def valid?
|
6
|
+
valid_v1? || valid_v2?
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def valid_v1?
|
12
|
+
return false unless /\A[0-9]{1,8}[A-Z]\z/.match?(tin)
|
13
|
+
|
14
|
+
tin[-1] == check_letter(tin)
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid_v2?
|
18
|
+
return false unless /\A[XYZKLM][0-9]{7}[A-Z]\z/.match?(tin)
|
19
|
+
|
20
|
+
tin[-1] == check_v2
|
21
|
+
end
|
22
|
+
|
23
|
+
def check_v2
|
24
|
+
# 0. Replace the leading letter by the corresponding digit and concatenate
|
25
|
+
# the result with the other characters:
|
26
|
+
digit =
|
27
|
+
case tin[0]
|
28
|
+
when "X", "K", "L", "M" then 0
|
29
|
+
when "Y" then 1
|
30
|
+
when "Z" then 2
|
31
|
+
end
|
32
|
+
|
33
|
+
check_letter("#{digit}#{tin[1..]}")
|
34
|
+
end
|
35
|
+
|
36
|
+
def check_letter(code)
|
37
|
+
# 1. Take the remainder of modulo 23 of the 8 first characters;
|
38
|
+
remainder = code[..-2].to_i % 23
|
39
|
+
|
40
|
+
# 2. Add 1 to the remainder of operation 1;
|
41
|
+
# 3. The check letter corresponds to this figure in the table below:
|
42
|
+
"TRWAGMYFPDXBNJZSQVHLCKE"[remainder]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TinValid
|
4
|
+
class UnitedKingdomTin < Data.define(:tin)
|
5
|
+
def valid?
|
6
|
+
valid_v1? || valid_v2?
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def valid_v1? = /\A[0-9]{10}\z/.match?(tin)
|
12
|
+
|
13
|
+
def valid_v2?
|
14
|
+
return false unless /\A[A-Z]{2}[0-9]{6}[A-D]?\z/.match?(tin)
|
15
|
+
return false if %w[D F I Q U V].include?(tin[0])
|
16
|
+
return false if %w[D F I Q O U V].include?(tin[1])
|
17
|
+
return false if %w[GB NK TN ZZ].include?(tin[0..1])
|
18
|
+
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/tin_valid/version.rb
CHANGED
data/lib/tin_valid.rb
CHANGED
@@ -10,9 +10,23 @@ require_relative "tin_valid/cyprus_tin"
|
|
10
10
|
require_relative "tin_valid/czechia_tin"
|
11
11
|
require_relative "tin_valid/denmark_tin"
|
12
12
|
require_relative "tin_valid/estonia_tin"
|
13
|
+
require_relative "tin_valid/germany_tin"
|
14
|
+
require_relative "tin_valid/greece_tin"
|
15
|
+
require_relative "tin_valid/hungary_tin"
|
16
|
+
require_relative "tin_valid/ireland_tin"
|
17
|
+
require_relative "tin_valid/italy_tin"
|
18
|
+
require_relative "tin_valid/latvia_tin"
|
19
|
+
require_relative "tin_valid/lithuania_tin"
|
20
|
+
require_relative "tin_valid/luxembourg_tin"
|
21
|
+
require_relative "tin_valid/malta_tin"
|
22
|
+
require_relative "tin_valid/netherlands_tin"
|
23
|
+
require_relative "tin_valid/poland_tin"
|
24
|
+
require_relative "tin_valid/portugal_tin"
|
25
|
+
require_relative "tin_valid/romania_tin"
|
26
|
+
require_relative "tin_valid/slovakia_tin"
|
27
|
+
require_relative "tin_valid/slovenia_tin"
|
28
|
+
require_relative "tin_valid/spain_tin"
|
29
|
+
require_relative "tin_valid/united_kingdom_tin"
|
13
30
|
require_relative "tin_valid/sweden_tin"
|
14
31
|
|
15
|
-
module TinValid
|
16
|
-
class Error < StandardError; end
|
17
|
-
# Your code goes here...
|
18
|
-
end
|
32
|
+
module TinValid; end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tin_valid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sunny Ripert
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-04-
|
10
|
+
date: 2025-04-17 00:00:00.000000000 Z
|
11
11
|
dependencies: []
|
12
12
|
email:
|
13
13
|
- sunny@sunfox.org
|
@@ -31,7 +31,24 @@ files:
|
|
31
31
|
- lib/tin_valid/czechia_tin.rb
|
32
32
|
- lib/tin_valid/denmark_tin.rb
|
33
33
|
- lib/tin_valid/estonia_tin.rb
|
34
|
+
- lib/tin_valid/germany_tin.rb
|
35
|
+
- lib/tin_valid/greece_tin.rb
|
36
|
+
- lib/tin_valid/hungary_tin.rb
|
37
|
+
- lib/tin_valid/ireland_tin.rb
|
38
|
+
- lib/tin_valid/italy_tin.rb
|
39
|
+
- lib/tin_valid/latvia_tin.rb
|
40
|
+
- lib/tin_valid/lithuania_tin.rb
|
41
|
+
- lib/tin_valid/luxembourg_tin.rb
|
42
|
+
- lib/tin_valid/malta_tin.rb
|
43
|
+
- lib/tin_valid/netherlands_tin.rb
|
44
|
+
- lib/tin_valid/poland_tin.rb
|
45
|
+
- lib/tin_valid/portugal_tin.rb
|
46
|
+
- lib/tin_valid/romania_tin.rb
|
47
|
+
- lib/tin_valid/slovakia_tin.rb
|
48
|
+
- lib/tin_valid/slovenia_tin.rb
|
49
|
+
- lib/tin_valid/spain_tin.rb
|
34
50
|
- lib/tin_valid/sweden_tin.rb
|
51
|
+
- lib/tin_valid/united_kingdom_tin.rb
|
35
52
|
- lib/tin_valid/version.rb
|
36
53
|
- sig/tin_valid.rbs
|
37
54
|
homepage: https://github.com/cults/tin_valid
|