tin_valid 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b248c0be6699ce56b48b082c320833ff8cb5594a62762d298ea66315d8d7555b
4
- data.tar.gz: a04549a7599fbadd881f1f905e2c13235d83a3e1d4b7bd8d79dc16393cd18465
3
+ metadata.gz: c64b5917083db71c567a2b80c3edf35d58d32b07da94b9e0ced0e189a3bb4f13
4
+ data.tar.gz: 9785af41b29fc7c335d1f7f062ed8ae217a421e145005a783bd56eeb43d75c5a
5
5
  SHA512:
6
- metadata.gz: 608e79cfe16d11cd236c02a4b5dffa9062a4b26b0f8816c2e9a9da350471917a5cc7ac6802202223ec6936f82cc0f91f9b820118a9c56868b2c6c4a9eaa8f67f
7
- data.tar.gz: a17c2624d59049524fbeb5b8adec5263dd6c4009d7bb90c594ad01c499c990a6784515a195bbd3a16148278c32f524b71067b6f0095020dccd24ae5a65987da4
6
+ metadata.gz: '097ef7fcbdeb7f95b0456fdcd930501f9da4eb85ddb9bc0a5a8e9996d58aa03939fe409d2a0ed7f0c25ae1bee786bcce19a3c079e91969b62839337d581f5d3a'
7
+ data.tar.gz: e4bb19d3238b04228390f78e505375af8e695f2cb3060c518f6aa6e41b2b61db2f065fc483ade6a84f45213e05c90f55f76b4a18f36f54b1a0615f505bf08e05
data/.rspec CHANGED
@@ -1,3 +1,2 @@
1
- --format documentation
2
1
  --color
3
2
  --require spec_helper
data/.rubocop.yml CHANGED
@@ -8,6 +8,11 @@ AllCops:
8
8
 
9
9
  # Layout
10
10
 
11
+ Layout/LineLength:
12
+ Max: 80
13
+ Exclude:
14
+ - bin/rspec
15
+
11
16
  Layout/MultilineMethodCallIndentation:
12
17
  # On multiline calls, indent starting from the receiver.
13
18
  # E.g.:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
+
3
20
  ## [0.1.1] - 2025-04-15
4
21
 
5
22
  Features:
@@ -7,7 +24,6 @@ Features:
7
24
  - Add Greece 🇬🇷
8
25
  - Add Hungary 🇭🇺
9
26
  - Add Ireland 🇮🇪
10
- - Add #normalized to Austria 🇦🇹
11
27
 
12
28
  ## [0.1.0] - 2025-04-14
13
29
 
data/README.md CHANGED
@@ -14,7 +14,20 @@ Validate Tax Identification Numbers (TINs) for the following European countries:
14
14
  - Greece 🇬🇷
15
15
  - Hungary 🇭🇺
16
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 🇪🇸
17
29
  - Sweden 🇸🇪
30
+ - United Kingdom 🇬🇧
18
31
 
19
32
  See also the [descriptions of the structure provided by the European
20
33
  Union](https://taxation-customs.ec.europa.eu/online-services/online-services-and-databases-taxation/taxpayer-identification-number-tin_en).
@@ -38,8 +51,7 @@ gem install tin_valid
38
51
 
39
52
  ```rb
40
53
  # Austria
41
- TinValid::AustriaTin.new(tin: "93-173/6581").valid? # => true
42
- TinValid::AustriaTin.new(tin: "93-173/6581").normalized # => "931736581"
54
+ TinValid::AustriaTin.new(tin: "").valid? # => true
43
55
 
44
56
  # Belgium
45
57
  # Optional birth_date
@@ -68,9 +80,67 @@ TinValid::DenmarkTin.new(tin: "…", birth_date: Date.new(…)).valid?
68
80
  # Optional birth_date
69
81
  TinValid::EstoniaTin.new(tin: "…", birth_date: Date.new(…)).valid?
70
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
+
71
138
  # Sweden
72
139
  # Optional birth_date
73
140
  TinValid::SwedenTin.new(tin: "…", birth_date: Date.new(…)).valid?
141
+
142
+ # United Kingdom
143
+ TinValid::UnitedKingdomTin.new(tin: "…").valid?
74
144
  ```
75
145
 
76
146
  ## Development
@@ -48,13 +48,16 @@ module TinValid
48
48
  # 3. Multiply the result by 2;
49
49
  result *= 2
50
50
 
51
- # 4. Take modulo 11 of the result. Update the value of variable X with the result of this operation;
51
+ # 4. Take modulo 11 of the result. Update the value of variable X with
52
+ # the result of this operation;
52
53
  x = result % 11
53
54
 
54
55
  # 5. Take C2 + X modulo 10. If result is 0, result is 10;
55
56
  # 6. Multiply the result by 2;
56
- # 7. Take modulo 11 of the result. Update the value of variable X with the result of this operation;
57
- # 8. Apply steps 5, 6 and 7 in an analogue way for digits C3 to C10. Consider that last value called Y;
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;
58
61
  x
59
62
  end
60
63
 
@@ -11,8 +11,6 @@ module TinValid
11
11
  private
12
12
 
13
13
  def check
14
- tin[..-2].chars
15
-
16
14
  # 1. Multiply the values of each position by the corresponding weight:
17
15
  # 2. Add up the results of the above multiplications;
18
16
  result = (1..9).each_with_index.sum { |num, i| num * tin[i].to_i }
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TinValid
4
- VERSION = "0.1.1"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/tin_valid.rb CHANGED
@@ -14,6 +14,19 @@ require_relative "tin_valid/germany_tin"
14
14
  require_relative "tin_valid/greece_tin"
15
15
  require_relative "tin_valid/hungary_tin"
16
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"
17
30
  require_relative "tin_valid/sweden_tin"
18
31
 
19
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: 0.1.1
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-15 00:00:00.000000000 Z
10
+ date: 2025-04-17 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  email:
13
13
  - sunny@sunfox.org
@@ -35,7 +35,20 @@ files:
35
35
  - lib/tin_valid/greece_tin.rb
36
36
  - lib/tin_valid/hungary_tin.rb
37
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
38
50
  - lib/tin_valid/sweden_tin.rb
51
+ - lib/tin_valid/united_kingdom_tin.rb
39
52
  - lib/tin_valid/version.rb
40
53
  - sig/tin_valid.rbs
41
54
  homepage: https://github.com/cults/tin_valid