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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5b010dba783fef5da5341515542f830f73ac1baeca9fd8334fee16867d07a5a
4
- data.tar.gz: 42d87f203dd942dc281636c0f3ff6b21e4104d9ae22904974cc81d7f503ead3a
3
+ metadata.gz: c64b5917083db71c567a2b80c3edf35d58d32b07da94b9e0ced0e189a3bb4f13
4
+ data.tar.gz: 9785af41b29fc7c335d1f7f062ed8ae217a421e145005a783bd56eeb43d75c5a
5
5
  SHA512:
6
- metadata.gz: 886d2bfe8eb377af5fed28166a3a97f2a56f0c5dddea52c06f5e844d0a6d7194e73750a601422ce9e2a90ed93fe5f7ee99f323c6af56c375d53b5b4a83bb1b30
7
- data.tar.gz: '09cd02962705707d397f82dba292263a5414ecf1993bb67c5d87645f6f782a50a74cfe75c97e3cddba494767a8442de2ad6515872b0b5a60d16e4ad22d6f5875'
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,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
- - Sweden
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
- Install the gem and add to the application's Gemfile by executing:
37
+ Add the gem to your applications 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/sunny/tin_valid. This project is intended to be a safe,
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/sunny/tin_valid/blob/main/CODE_OF_CONDUCT.md).
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/sunny/tin_valid/blob/main/CODE_OF_CONDUCT.md).
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 < Data.define(:tin)
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 /\A[0-9]{9}\z/.match?(tin)
12
+ return false unless MATCHER.match?(normalized)
7
13
 
8
- tin[-1] == check
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
- tin
26
- .chars
38
+ normalized
39
+ .chars[..-2]
27
40
  .each_with_index
28
- .map { |n, i| n.to_i * (i.even? ? 2 : 1) }
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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TinValid
4
+ class GreeceTin < Data.define(:tin)
5
+ def valid?
6
+ /\A[0-9]{9}\z/.match?(tin)
7
+ end
8
+ end
9
+ 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TinValid
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
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: 0.1.0
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-14 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
@@ -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