israeli 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 775d9b34a1173bd2bbab00ca4a86f13d8fb340c13ee84d15f5893cdc7d1d6dab
4
+ data.tar.gz: 6b872c2ebbd7bd5573854e20f79e5bbd1ae5de265a879f370af625a0a2a00d8a
5
+ SHA512:
6
+ metadata.gz: d5d9bc4d5d2b1e874221c84612307533dfc252a077c2fe5c388cfb55ca4283f39e180656353697ac38e65285a1f700bc1786f37791ff8b401f09e1252255852e
7
+ data.tar.gz: de83f4a136c0f53111ba505cd9165c02fac56e044a2e3d9d2819a9d459e91b4a792c50fd6b8090dc2365cc1b12de0f98d3a45225fb3685b0a2088198205c211f
data/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-12-03
11
+
12
+ ### Added
13
+
14
+ - **Israeli ID Validator (Mispar Zehut)**
15
+ - 9-digit validation with Luhn algorithm checksum
16
+ - Automatic left-padding for shorter inputs
17
+ - Format stripping (spaces, hyphens)
18
+ - `Israeli.valid_id?` and `Israeli.format_id` methods
19
+ - `IsraeliIdValidator` for ActiveModel/Rails
20
+
21
+ - **Phone Number Validator**
22
+ - Mobile numbers (05X prefix, 10 digits)
23
+ - Landline numbers (02-09 area codes, 9 digits)
24
+ - VoIP numbers (07X prefix, 10 digits)
25
+ - International format support (+972 prefix)
26
+ - `Israeli.valid_phone?` with `:type` option
27
+ - `IsraeliPhoneValidator` for ActiveModel/Rails with `:type` option
28
+
29
+ - **Postal Code Validator (Mikud)**
30
+ - 7-digit format validation
31
+ - Space separator support
32
+ - `Israeli.valid_postal_code?` method
33
+ - `IsraeliPostalCodeValidator` for ActiveModel/Rails
34
+
35
+ - **Bank Account Validator**
36
+ - Domestic format (13 digits: 2-3-8 structure)
37
+ - IBAN format with mod 97 checksum validation
38
+ - `Israeli.valid_bank_account?` with `:format` option
39
+ - `IsraeliBankAccountValidator` for ActiveModel/Rails with `:format` option
40
+
41
+ - **Core Utilities**
42
+ - `Israeli::Luhn` - Luhn mod 10 checksum algorithm
43
+ - `Israeli::Sanitizer` - Input normalization utilities
44
+ - `Israeli::Railtie` - Automatic Rails integration
45
+
46
+ - **Rails Integration**
47
+ - All validators support `allow_nil`, `allow_blank`, and `message` options
48
+ - Automatic loading via Railtie in Rails applications
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 dpaluy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # Israeli
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/israeli.svg)](https://rubygems.org/gems/israeli)
4
+ [![CI](https://github.com/dpaluy/israeli/actions/workflows/ci.yml/badge.svg)](https://github.com/dpaluy/israeli/actions/workflows/ci.yml)
5
+
6
+ Validation utilities for Israeli identifiers including ID numbers (Mispar Zehut), phone numbers, postal codes, and bank accounts.
7
+
8
+ ## Features
9
+
10
+ - **Israeli ID (Mispar Zehut)** - 9-digit validation with Luhn checksum
11
+ - **Phone Numbers** - Mobile (05X), landline (02-09), and VoIP (07X)
12
+ - **Postal Codes** - 7-digit Israeli postal codes (Mikud)
13
+ - **Bank Accounts** - Domestic (13-digit) and IBAN formats with mod 97 validation
14
+ - **Rails Integration** - ActiveModel validators with standard options
15
+ - **Zero Dependencies** - Pure Ruby, no external runtime dependencies
16
+
17
+ ## Installation
18
+
19
+ Add to your Gemfile:
20
+
21
+ ```ruby
22
+ gem "israeli"
23
+ ```
24
+
25
+ Then run:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ Or install directly:
32
+
33
+ ```bash
34
+ gem install israeli
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Standalone Validation
40
+
41
+ ```ruby
42
+ require "israeli"
43
+
44
+ # Israeli ID (Mispar Zehut)
45
+ Israeli.valid_id?("123456782") # => true
46
+ Israeli.valid_id?("12345678-2") # => true (formatted input OK)
47
+ Israeli.valid_id?("123456789") # => false (invalid checksum)
48
+
49
+ # Phone Numbers
50
+ Israeli.valid_phone?("0501234567") # => true (any type)
51
+ Israeli.valid_phone?("0501234567", type: :mobile) # => true
52
+ Israeli.valid_phone?("+972501234567", type: :mobile) # => true (international format)
53
+ Israeli.valid_phone?("021234567", type: :landline) # => true (Jerusalem)
54
+ Israeli.valid_phone?("0721234567", type: :voip) # => true
55
+
56
+ # Postal Codes
57
+ Israeli.valid_postal_code?("2610101") # => true
58
+ Israeli.valid_postal_code?("26101 01") # => true (with space)
59
+
60
+ # Bank Accounts
61
+ Israeli.valid_bank_account?("4985622815429") # => true (domestic)
62
+ Israeli.valid_bank_account?("49-856-22815429") # => true (formatted)
63
+ Israeli.valid_bank_account?("IL620108000000099999999") # => true (IBAN)
64
+ Israeli.valid_bank_account?("IL62 0108 0000 0009 9999 999") # => true (spaced IBAN)
65
+ ```
66
+
67
+ ### Formatting
68
+
69
+ ```ruby
70
+ # Format ID to 9 digits
71
+ Israeli.format_id("12345678-2") # => "123456782"
72
+ Israeli.format_id("12345678") # => "012345678" (pads with zero)
73
+
74
+ # Format phone numbers
75
+ Israeli.format_phone("0501234567") # => "050-123-4567"
76
+ Israeli.format_phone("0501234567", style: :international) # => "+972-501234567"
77
+ Israeli.format_phone("021234567") # => "02-123-4567"
78
+ ```
79
+
80
+ ### Rails / ActiveModel Integration
81
+
82
+ When used in a Rails application, validators are automatically loaded via Railtie.
83
+
84
+ ```ruby
85
+ class Person < ApplicationRecord
86
+ validates :id_number, israeli_id: true
87
+ validates :mobile_phone, israeli_phone: { type: :mobile }
88
+ validates :postal_code, israeli_postal_code: { allow_blank: true }
89
+ validates :bank_account, israeli_bank_account: { format: :iban }
90
+ end
91
+ ```
92
+
93
+ #### Validator Options
94
+
95
+ All validators support standard ActiveModel options:
96
+
97
+ ```ruby
98
+ # Allow nil/blank values
99
+ validates :id_number, israeli_id: { allow_nil: true }
100
+ validates :postal_code, israeli_postal_code: { allow_blank: true }
101
+
102
+ # Custom error messages
103
+ validates :id_number, israeli_id: { message: "is not a valid Mispar Zehut" }
104
+
105
+ # Phone type restriction
106
+ validates :mobile, israeli_phone: { type: :mobile } # Mobile only (05X)
107
+ validates :office, israeli_phone: { type: :landline } # Landline only (02-09)
108
+ validates :voip, israeli_phone: { type: :voip } # VoIP only (072-079)
109
+
110
+ # Bank account format restriction
111
+ validates :account, israeli_bank_account: { format: :domestic } # 13-digit only
112
+ validates :iban, israeli_bank_account: { format: :iban } # IBAN only
113
+ ```
114
+
115
+ ### Direct Validator Access
116
+
117
+ For more control, you can use the validator classes directly:
118
+
119
+ ```ruby
120
+ # ID Validator
121
+ Israeli::Validators::Id.valid?("123456782") # => true
122
+ Israeli::Validators::Id.format("12345678-2") # => "123456782"
123
+
124
+ # Phone Validator
125
+ Israeli::Validators::Phone.valid?("0501234567", type: :mobile) # => true
126
+ Israeli::Validators::Phone.mobile?("0501234567") # => true
127
+ Israeli::Validators::Phone.landline?("021234567") # => true
128
+
129
+ # Postal Code Validator
130
+ Israeli::Validators::PostalCode.valid?("2610101") # => true
131
+ Israeli::Validators::PostalCode.format("2610101", style: :spaced) # => "26101 01"
132
+
133
+ # Bank Account Validator
134
+ Israeli::Validators::BankAccount.valid?("4985622815429", format: :domestic) # => true
135
+ Israeli::Validators::BankAccount.valid_iban?("IL620108000000099999999") # => true
136
+ ```
137
+
138
+ ## Validation Rules
139
+
140
+ ### Israeli ID (Mispar Zehut)
141
+ - Exactly 9 digits (shorter inputs are left-padded with zeros)
142
+ - Validated using Luhn algorithm (mod 10 checksum)
143
+ - Accepts formatted input (spaces, hyphens are stripped)
144
+
145
+ ### Phone Numbers
146
+
147
+ | Type | Format | Example |
148
+ |------|--------|---------|
149
+ | Mobile | 05X-XXX-XXXX (10 digits) | 050-123-4567 |
150
+ | Landline | 0X-XXX-XXXX (9 digits) | 02-123-4567 |
151
+ | VoIP | 07X-XXX-XXXX (10 digits) | 072-123-4567 |
152
+
153
+ - International format (+972) is automatically converted to domestic format
154
+ - Area codes: 02 (Jerusalem), 03 (Tel Aviv), 04 (Haifa), 08 (South), 09 (Sharon)
155
+
156
+ ### Postal Codes
157
+ - Exactly 7 digits
158
+ - Format validation only (no geographic range checking)
159
+ - Accepts with or without space separator
160
+
161
+ ### Bank Accounts
162
+
163
+ | Format | Structure | Example |
164
+ |--------|-----------|---------|
165
+ | Domestic | XX-XXX-XXXXXXXX (13 digits) | 49-856-22815429 |
166
+ | IBAN | IL + 2 check + 19 digits (23 chars) | IL62 0108 0000 0009 9999 999 |
167
+
168
+ - IBAN validated with mod 97 checksum
169
+
170
+ ## Development
171
+
172
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
173
+
174
+ ```bash
175
+ bundle install
176
+ bundle exec rake test # Run tests
177
+ bundle exec rubocop # Run linter
178
+ bundle exec rake # Run both
179
+ ```
180
+
181
+ ## Contributing
182
+
183
+ Bug reports and pull requests are welcome on GitHub at https://github.com/dpaluy/israeli.
184
+
185
+ ## License
186
+
187
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ # Validates that an attribute is a valid Israeli bank account number.
6
+ #
7
+ # @example Basic usage (accepts domestic or IBAN)
8
+ # class BankAccount < ApplicationRecord
9
+ # validates :account_number, israeli_bank_account: true
10
+ # end
11
+ #
12
+ # @example Domestic format only
13
+ # class BankAccount < ApplicationRecord
14
+ # validates :domestic_account, israeli_bank_account: { format: :domestic }
15
+ # end
16
+ #
17
+ # @example IBAN format only
18
+ # class BankAccount < ApplicationRecord
19
+ # validates :iban, israeli_bank_account: { format: :iban }
20
+ # end
21
+ class IsraeliBankAccountValidator < ActiveModel::EachValidator
22
+ def validate_each(record, attribute, value)
23
+ return if value.blank? && options[:allow_blank]
24
+ return if value.nil? && options[:allow_nil]
25
+
26
+ account_format = options[:format] || :any
27
+ return if Israeli::Validators::BankAccount.valid?(value, format: account_format)
28
+
29
+ record.errors.add(
30
+ attribute,
31
+ options[:message] || :invalid
32
+ )
33
+ end
34
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ # Validates that an attribute is a valid Israeli ID number (Mispar Zehut).
6
+ #
7
+ # @example Basic usage
8
+ # class Person < ApplicationRecord
9
+ # validates :id_number, israeli_id: true
10
+ # end
11
+ #
12
+ # @example With options
13
+ # class Person < ApplicationRecord
14
+ # validates :id_number, israeli_id: { allow_blank: true, message: "is not a valid Israeli ID" }
15
+ # end
16
+ class IsraeliIdValidator < ActiveModel::EachValidator
17
+ def validate_each(record, attribute, value)
18
+ return if value.blank? && options[:allow_blank]
19
+ return if value.nil? && options[:allow_nil]
20
+
21
+ return if Israeli::Validators::Id.valid?(value)
22
+
23
+ record.errors.add(
24
+ attribute,
25
+ options[:message] || :invalid
26
+ )
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ # Validates that an attribute is a valid Israeli phone number.
6
+ #
7
+ # @example Basic usage (accepts mobile, landline, or VoIP)
8
+ # class Contact < ApplicationRecord
9
+ # validates :phone, israeli_phone: true
10
+ # end
11
+ #
12
+ # @example Mobile only
13
+ # class Contact < ApplicationRecord
14
+ # validates :mobile_phone, israeli_phone: { type: :mobile }
15
+ # end
16
+ #
17
+ # @example Landline only
18
+ # class Contact < ApplicationRecord
19
+ # validates :office_phone, israeli_phone: { type: :landline }
20
+ # end
21
+ #
22
+ # @example VoIP only
23
+ # class Contact < ApplicationRecord
24
+ # validates :voip_number, israeli_phone: { type: :voip }
25
+ # end
26
+ class IsraeliPhoneValidator < ActiveModel::EachValidator
27
+ def validate_each(record, attribute, value)
28
+ return if value.blank? && options[:allow_blank]
29
+ return if value.nil? && options[:allow_nil]
30
+
31
+ phone_type = options[:type] || :any
32
+ return if Israeli::Validators::Phone.valid?(value, type: phone_type)
33
+
34
+ record.errors.add(
35
+ attribute,
36
+ options[:message] || :invalid
37
+ )
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ # Validates that an attribute is a valid Israeli postal code (Mikud).
6
+ #
7
+ # @example Basic usage
8
+ # class Address < ApplicationRecord
9
+ # validates :postal_code, israeli_postal_code: true
10
+ # end
11
+ #
12
+ # @example With options
13
+ # class Address < ApplicationRecord
14
+ # validates :postal_code, israeli_postal_code: { allow_blank: true }
15
+ # end
16
+ class IsraeliPostalCodeValidator < ActiveModel::EachValidator
17
+ def validate_each(record, attribute, value)
18
+ return if value.blank? && options[:allow_blank]
19
+ return if value.nil? && options[:allow_nil]
20
+
21
+ return if Israeli::Validators::PostalCode.valid?(value)
22
+
23
+ record.errors.add(
24
+ attribute,
25
+ options[:message] || :invalid
26
+ )
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Israeli
4
+ # Base error class for all Israeli validation errors.
5
+ #
6
+ # All validation-related exceptions inherit from this class.
7
+ #
8
+ # @example Handling errors
9
+ # begin
10
+ # Israeli.format_id!("invalid")
11
+ # rescue Israeli::Error => e
12
+ # puts "Validation failed: #{e.message}"
13
+ # end
14
+ class Error < StandardError; end
15
+
16
+ # Raised when input format is invalid for the requested validation type.
17
+ #
18
+ # @example
19
+ # Israeli.format_id!("abc")
20
+ # # => Israeli::InvalidFormatError: ID must contain only digits
21
+ class InvalidFormatError < Error; end
22
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Israeli
4
+ # Luhn algorithm (mod 10) implementation for checksum validation.
5
+ #
6
+ # Used by Israeli ID numbers (Mispar Zehut) and potentially business
7
+ # registration numbers. The Israeli variant uses a left-to-right
8
+ # processing order (index 0 = multiplier 1, index 1 = multiplier 2, etc.).
9
+ #
10
+ # @see https://en.wikipedia.org/wiki/Luhn_algorithm
11
+ # @see https://en.wikipedia.org/wiki/Israeli_identity_card
12
+ module Luhn
13
+ # Validates a string of digits using the Luhn algorithm.
14
+ #
15
+ # @param digits [String] A string containing only digits (0-9)
16
+ # @return [Boolean] true if the checksum is valid, false otherwise
17
+ #
18
+ # @example Valid ID
19
+ # Israeli::Luhn.valid?("123456782") # => true
20
+ #
21
+ # @example Invalid ID
22
+ # Israeli::Luhn.valid?("123456789") # => false
23
+ def self.valid?(digits)
24
+ return false if digits.nil? || digits.empty?
25
+ return false unless digits.match?(/\A\d+\z/)
26
+
27
+ sum = digits.chars.each_with_index.sum do |char, index|
28
+ digit = char.to_i * (index.even? ? 1 : 2)
29
+ digit > 9 ? digit - 9 : digit
30
+ end
31
+
32
+ (sum % 10).zero?
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Israeli
4
+ # Rails integration for Israeli validators.
5
+ #
6
+ # Automatically loads ActiveModel validators when used in a Rails application.
7
+ # The validators are loaded via ActiveSupport's lazy load hooks to ensure
8
+ # proper initialization timing.
9
+ #
10
+ # @example Usage in a Rails model
11
+ # class Person < ApplicationRecord
12
+ # validates :id_number, israeli_id: true
13
+ # validates :phone, israeli_phone: { type: :mobile }
14
+ # validates :postal_code, israeli_postal_code: { allow_blank: true }
15
+ # validates :bank_account, israeli_bank_account: { format: :iban }
16
+ # end
17
+ class Railtie < Rails::Railtie
18
+ initializer "israeli.active_model" do
19
+ ActiveSupport.on_load(:active_model) do
20
+ require "israeli/active_model/israeli_id_validator"
21
+ require "israeli/active_model/israeli_postal_code_validator"
22
+ require "israeli/active_model/israeli_phone_validator"
23
+ require "israeli/active_model/israeli_bank_account_validator"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Israeli
4
+ # Input sanitization utilities for Israeli validators.
5
+ #
6
+ # Provides consistent normalization of user input across all validators,
7
+ # handling common formatting variations like spaces, hyphens, and
8
+ # international phone prefixes.
9
+ module Sanitizer
10
+ # Common separator characters to strip from input
11
+ STRIP_CHARS = /[\s\-.+]/
12
+
13
+ # Extracts only digit characters from input.
14
+ #
15
+ # Strips common formatting characters (spaces, hyphens, dots) and
16
+ # converts the input to a string. Returns nil for nil input.
17
+ #
18
+ # @param value [String, Integer, nil] The value to sanitize
19
+ # @return [String, nil] String containing only digits, or nil
20
+ #
21
+ # @example Basic usage
22
+ # Sanitizer.digits_only("123-456-789") # => "123456789"
23
+ # Sanitizer.digits_only("12 34 56") # => "123456"
24
+ # Sanitizer.digits_only(123456) # => "123456"
25
+ # Sanitizer.digits_only(nil) # => nil
26
+ def self.digits_only(value)
27
+ return nil if value.nil?
28
+
29
+ value.to_s.gsub(STRIP_CHARS, "")
30
+ end
31
+
32
+ # Normalizes phone numbers by stripping international prefixes.
33
+ #
34
+ # Handles common Israeli international dialing formats:
35
+ # - +972 (standard international)
36
+ # - 972 (without plus)
37
+ # - 00972 (international access code)
38
+ #
39
+ # @param value [String, nil] The phone number to normalize
40
+ # @return [String, nil] Normalized domestic phone number, or nil
41
+ #
42
+ # @example International formats
43
+ # Sanitizer.normalize_phone("+972501234567") # => "0501234567"
44
+ # Sanitizer.normalize_phone("972-50-123-4567") # => "0501234567"
45
+ # Sanitizer.normalize_phone("00972501234567") # => "0501234567"
46
+ #
47
+ # @example Domestic formats (unchanged)
48
+ # Sanitizer.normalize_phone("050-123-4567") # => "0501234567"
49
+ # Sanitizer.normalize_phone("0501234567") # => "0501234567"
50
+ def self.normalize_phone(value)
51
+ cleaned = digits_only(value)
52
+ return nil if cleaned.nil?
53
+
54
+ # Strip international prefixes and add leading zero
55
+ cleaned.sub(/\A(00972|972)/, "0")
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Israeli
4
+ module Validators
5
+ # Validates Israeli bank account numbers.
6
+ #
7
+ # Supports two formats:
8
+ # - Domestic: 13 digits (2-digit bank + 3-digit branch + 8-digit account)
9
+ # - IBAN: 23 characters (IL + 2 check digits + 19 BBAN digits)
10
+ #
11
+ # @example Domestic validation
12
+ # Israeli::Validators::BankAccount.valid?("4985622815429") # => true
13
+ # Israeli::Validators::BankAccount.valid?("49-856-22815429") # => true
14
+ #
15
+ # @example IBAN validation
16
+ # Israeli::Validators::BankAccount.valid?("IL620108000000099999999") # => true
17
+ #
18
+ # @see https://www.ecbs.org/iban/israel-bank-account-number.html
19
+ class BankAccount
20
+ # Domestic format: 2 (bank) + 3 (branch) + 8 (account) = 13 digits
21
+ DOMESTIC_PATTERN = /\A\d{13}\z/
22
+
23
+ # IBAN format: IL + 2 check digits + 19 BBAN digits = 23 characters
24
+ IBAN_PATTERN = /\AIL\d{21}\z/
25
+
26
+ # Validates an Israeli bank account number.
27
+ #
28
+ # @param value [String, nil] The bank account to validate
29
+ # @param format [Symbol] Format to validate: :domestic, :iban, or :any
30
+ # @return [Boolean] true if valid, false otherwise
31
+ def self.valid?(value, format: :any)
32
+ return false if value.nil?
33
+
34
+ case format
35
+ when :domestic
36
+ valid_domestic?(Sanitizer.digits_only(value))
37
+ when :iban
38
+ valid_iban?(normalize_iban(value))
39
+ when :any
40
+ valid_domestic?(Sanitizer.digits_only(value)) ||
41
+ valid_iban?(normalize_iban(value))
42
+ else
43
+ false
44
+ end
45
+ end
46
+
47
+ # Validates a domestic Israeli bank account (13 digits).
48
+ #
49
+ # @param digits [String, nil] Digits-only bank account
50
+ # @return [Boolean]
51
+ def self.valid_domestic?(digits)
52
+ return false if digits.nil?
53
+
54
+ digits.match?(DOMESTIC_PATTERN)
55
+ end
56
+
57
+ # Validates an Israeli IBAN with mod 97 checksum.
58
+ #
59
+ # @param iban [String, nil] Normalized IBAN (uppercase, no spaces)
60
+ # @return [Boolean]
61
+ def self.valid_iban?(iban)
62
+ return false if iban.nil?
63
+ return false unless iban.match?(IBAN_PATTERN)
64
+
65
+ # IBAN mod 97 validation
66
+ # 1. Move first 4 chars to end
67
+ # 2. Convert letters to numbers (A=10, B=11, ..., Z=35)
68
+ # 3. Calculate mod 97, must equal 1
69
+ rearranged = iban[4..] + iban[0..3]
70
+ numeric = rearranged.gsub(/[A-Z]/) { |c| (c.ord - 55).to_s }
71
+ numeric.to_i % 97 == 1
72
+ end
73
+
74
+ # Formats a bank account to a specified style.
75
+ #
76
+ # @param value [String, nil] The bank account to format
77
+ # @param style [Symbol] :domestic (XX-XXX-XXXXXXXX) or :iban
78
+ # @return [String, nil] Formatted bank account, or nil if invalid
79
+ #
80
+ # @example
81
+ # BankAccount.format("4985622815429") # => "49-856-22815429"
82
+ # BankAccount.format("4985622815429", style: :compact) # => "4985622815429"
83
+ def self.format(value, style: :domestic)
84
+ digits = Sanitizer.digits_only(value)
85
+
86
+ if valid_domestic?(digits)
87
+ case style
88
+ when :domestic
89
+ "#{digits[0..1]}-#{digits[2..4]}-#{digits[5..12]}"
90
+ else
91
+ digits
92
+ end
93
+ elsif valid_iban?(normalize_iban(value))
94
+ normalize_iban(value)
95
+ end
96
+ end
97
+
98
+ # @private
99
+ def self.normalize_iban(value)
100
+ value.to_s.gsub(/\s/, "").upcase
101
+ end
102
+ private_class_method :normalize_iban
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Israeli
4
+ module Validators
5
+ # Validates Israeli ID numbers (Mispar Zehut / Teudat Zehut).
6
+ #
7
+ # Israeli ID numbers are 9-digit numbers where the last digit is a
8
+ # check digit calculated using the Luhn algorithm (mod 10).
9
+ #
10
+ # @example Basic validation
11
+ # Israeli::Validators::Id.valid?("123456782") # => true
12
+ # Israeli::Validators::Id.valid?("123456789") # => false
13
+ #
14
+ # @example With formatting
15
+ # Israeli::Validators::Id.valid?("12345678-2") # => true
16
+ # Israeli::Validators::Id.valid?("012345678") # => true (leading zero)
17
+ #
18
+ # @see https://en.wikipedia.org/wiki/Israeli_identity_card
19
+ class Id
20
+ # Validates an Israeli ID number.
21
+ #
22
+ # Accepts formatted input (with hyphens/spaces) and handles leading zeros.
23
+ # IDs shorter than 9 digits are automatically left-padded with zeros.
24
+ #
25
+ # @param value [String, Integer, nil] The ID number to validate
26
+ # @return [Boolean] true if valid, false otherwise
27
+ def self.valid?(value)
28
+ digits = Sanitizer.digits_only(value)
29
+ return false if digits.nil? || digits.empty?
30
+
31
+ # Left-pad to 9 digits if shorter (handles IDs like "12345678")
32
+ padded = digits.rjust(9, "0")
33
+
34
+ # Must be exactly 9 digits after padding
35
+ return false unless padded.match?(/\A\d{9}\z/)
36
+
37
+ Luhn.valid?(padded)
38
+ end
39
+
40
+ # Formats an Israeli ID to standard 9-digit format.
41
+ #
42
+ # @param value [String, Integer, nil] The ID number to format
43
+ # @return [String, nil] 9-digit formatted ID, or nil if invalid
44
+ #
45
+ # @example
46
+ # Israeli::Validators::Id.format("12345678-2") # => "123456782"
47
+ # Israeli::Validators::Id.format(12345678) # => "012345678"
48
+ def self.format(value)
49
+ digits = Sanitizer.digits_only(value)
50
+ return nil if digits.nil? || digits.empty?
51
+
52
+ padded = digits.rjust(9, "0")
53
+ return nil unless valid?(padded)
54
+
55
+ padded
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Israeli
4
+ module Validators
5
+ # Validates Israeli phone numbers.
6
+ #
7
+ # Supports three types of Israeli phone numbers:
8
+ # - Mobile: 10 digits starting with 05X (050, 052, 053, 054, 055, 058)
9
+ # - Landline: 9 digits starting with 0X (02, 03, 04, 08, 09)
10
+ # - VoIP: 10 digits starting with 07X (072-079)
11
+ #
12
+ # Automatically handles international format (+972) conversion.
13
+ #
14
+ # @example Mobile validation
15
+ # Israeli::Validators::Phone.valid?("0501234567") # => true
16
+ # Israeli::Validators::Phone.valid?("+972501234567") # => true
17
+ # Israeli::Validators::Phone.valid?("050-123-4567") # => true
18
+ #
19
+ # @example Landline validation
20
+ # Israeli::Validators::Phone.valid?("021234567", type: :landline) # => true
21
+ # Israeli::Validators::Phone.valid?("03-123-4567") # => true
22
+ #
23
+ # @see https://en.wikipedia.org/wiki/Telephone_numbers_in_Israel
24
+ class Phone
25
+ # Mobile phone pattern: 05X followed by 7 more digits (10 total)
26
+ MOBILE_PATTERN = /\A05\d{8}\z/
27
+
28
+ # Landline pattern: 0X followed by 7 digits (9 total)
29
+ # Valid area codes: 02 (Jerusalem), 03 (Tel Aviv), 04 (Haifa), 08 (South), 09 (Sharon)
30
+ LANDLINE_PATTERN = /\A0[2-489]\d{7}\z/
31
+
32
+ # VoIP pattern: 07X (X=2-9) followed by 7 digits (10 total)
33
+ VOIP_PATTERN = /\A07[2-9]\d{7}\z/
34
+
35
+ # Validates an Israeli phone number.
36
+ #
37
+ # @param value [String, nil] The phone number to validate
38
+ # @param type [Symbol] Type to validate: :mobile, :landline, :voip, or :any
39
+ # @return [Boolean] true if valid, false otherwise
40
+ def self.valid?(value, type: :any)
41
+ normalized = Sanitizer.normalize_phone(value)
42
+ return false if normalized.nil? || normalized.empty?
43
+
44
+ case type
45
+ when :mobile then mobile?(normalized)
46
+ when :landline then landline?(normalized)
47
+ when :voip then voip?(normalized)
48
+ when :any then mobile?(normalized) || landline?(normalized) || voip?(normalized)
49
+ else false
50
+ end
51
+ end
52
+
53
+ # Checks if the number is a valid mobile number.
54
+ #
55
+ # @param value [String] Normalized phone number
56
+ # @return [Boolean]
57
+ def self.mobile?(value)
58
+ value.match?(MOBILE_PATTERN)
59
+ end
60
+
61
+ # Checks if the number is a valid landline number.
62
+ #
63
+ # @param value [String] Normalized phone number
64
+ # @return [Boolean]
65
+ def self.landline?(value)
66
+ value.match?(LANDLINE_PATTERN)
67
+ end
68
+
69
+ # Checks if the number is a valid VoIP number.
70
+ #
71
+ # @param value [String] Normalized phone number
72
+ # @return [Boolean]
73
+ def self.voip?(value)
74
+ value.match?(VOIP_PATTERN)
75
+ end
76
+
77
+ # Formats a phone number.
78
+ #
79
+ # @param value [String, nil] The phone number to format
80
+ # @param style [Symbol] :dashed, :international, or :compact
81
+ # @return [String, nil] Formatted phone number, or nil if invalid
82
+ #
83
+ # @example
84
+ # Phone.format("0501234567") # => "050-123-4567"
85
+ # Phone.format("0501234567", style: :international) # => "+972-50-123-4567"
86
+ # Phone.format("021234567") # => "02-123-4567"
87
+ def self.format(value, style: :dashed)
88
+ normalized = Sanitizer.normalize_phone(value)
89
+ return nil unless valid?(normalized)
90
+
91
+ case style
92
+ when :dashed
93
+ format_dashed(normalized)
94
+ when :international
95
+ "+972-#{normalized[1..]}"
96
+ else
97
+ normalized
98
+ end
99
+ end
100
+
101
+ # @private
102
+ def self.format_dashed(normalized)
103
+ if mobile?(normalized) || voip?(normalized)
104
+ # 10-digit format: XXX-XXX-XXXX
105
+ "#{normalized[0..2]}-#{normalized[3..5]}-#{normalized[6..9]}"
106
+ else
107
+ # 9-digit landline format: XX-XXX-XXXX
108
+ "#{normalized[0..1]}-#{normalized[2..4]}-#{normalized[5..8]}"
109
+ end
110
+ end
111
+ private_class_method :format_dashed
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Israeli
4
+ module Validators
5
+ # Validates Israeli postal codes (Mikud).
6
+ #
7
+ # Israeli postal codes consist of exactly 7 digits. They are organized
8
+ # geographically from north to south, with the first 2 digits indicating
9
+ # the postal area. Jerusalem postal codes start with 9 despite its
10
+ # central location.
11
+ #
12
+ # @example Basic validation
13
+ # Israeli::Validators::PostalCode.valid?("2610101") # => true
14
+ # Israeli::Validators::PostalCode.valid?("26101 01") # => true (with space)
15
+ #
16
+ # @example Regional examples
17
+ # Israeli::Validators::PostalCode.valid?("1029200") # => true (Metula, north)
18
+ # Israeli::Validators::PostalCode.valid?("8800000") # => true (Eilat, south)
19
+ # Israeli::Validators::PostalCode.valid?("9100000") # => true (Jerusalem)
20
+ #
21
+ # @see https://globepostalcodes.com/israel
22
+ class PostalCode
23
+ # Validates an Israeli postal code.
24
+ #
25
+ # @param value [String, nil] The postal code to validate
26
+ # @return [Boolean] true if valid 7-digit format, false otherwise
27
+ def self.valid?(value)
28
+ digits = Sanitizer.digits_only(value)
29
+ return false if digits.nil?
30
+
31
+ digits.match?(/\A\d{7}\z/)
32
+ end
33
+
34
+ # Formats a postal code to standard representation.
35
+ #
36
+ # @param value [String, nil] The postal code to format
37
+ # @param style [Symbol] :compact (7 digits) or :spaced (5+2 format)
38
+ # @return [String, nil] Formatted postal code, or nil if invalid
39
+ #
40
+ # @example
41
+ # Israeli::Validators::PostalCode.format("26101 01") # => "2610101"
42
+ # Israeli::Validators::PostalCode.format("2610101", style: :spaced) # => "26101 01"
43
+ def self.format(value, style: :compact)
44
+ digits = Sanitizer.digits_only(value)
45
+ return nil unless valid?(digits)
46
+
47
+ case style
48
+ when :spaced then "#{digits[0..4]} #{digits[5..6]}"
49
+ else digits
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Israeli
4
+ VERSION = "0.1.0"
5
+ end
data/lib/israeli.rb ADDED
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "israeli/version"
4
+ require_relative "israeli/errors"
5
+ require_relative "israeli/luhn"
6
+ require_relative "israeli/sanitizer"
7
+
8
+ # Main namespace for the Israeli validators gem.
9
+ #
10
+ # Provides validation utilities for Israeli identifiers including ID numbers
11
+ # (Mispar Zehut), postal codes, phone numbers, and bank accounts.
12
+ #
13
+ # @example Validate an Israeli ID
14
+ # Israeli.valid_id?("123456782") # => true
15
+ #
16
+ # @example Validate a phone number
17
+ # Israeli.valid_phone?("0501234567", type: :mobile) # => true
18
+ #
19
+ # @see Israeli::Validators::Id
20
+ # @see Israeli::Validators::Phone
21
+ # @see Israeli::Validators::PostalCode
22
+ # @see Israeli::Validators::BankAccount
23
+ module Israeli
24
+ class << self
25
+ # Validates an Israeli ID number (Mispar Zehut).
26
+ #
27
+ # @param value [String, Integer] The ID number to validate
28
+ # @return [Boolean] true if valid, false otherwise
29
+ #
30
+ # @example
31
+ # Israeli.valid_id?("123456782") # => true
32
+ # Israeli.valid_id?("12345678-2") # => true (formatted)
33
+ # Israeli.valid_id?("123456789") # => false (bad checksum)
34
+ def valid_id?(value)
35
+ Validators::Id.valid?(value)
36
+ end
37
+
38
+ # Validates an Israeli postal code (Mikud).
39
+ #
40
+ # @param value [String] The postal code to validate
41
+ # @return [Boolean] true if valid, false otherwise
42
+ #
43
+ # @example
44
+ # Israeli.valid_postal_code?("2610101") # => true
45
+ # Israeli.valid_postal_code?("26101 01") # => true (with space)
46
+ def valid_postal_code?(value)
47
+ Validators::PostalCode.valid?(value)
48
+ end
49
+
50
+ # Validates an Israeli phone number.
51
+ #
52
+ # @param value [String] The phone number to validate
53
+ # @param type [Symbol] Type of phone: :mobile, :landline, :voip, or :any
54
+ # @return [Boolean] true if valid, false otherwise
55
+ #
56
+ # @example
57
+ # Israeli.valid_phone?("0501234567") # => true
58
+ # Israeli.valid_phone?("0501234567", type: :mobile) # => true
59
+ # Israeli.valid_phone?("+972501234567", type: :mobile) # => true
60
+ def valid_phone?(value, type: :any)
61
+ Validators::Phone.valid?(value, type: type)
62
+ end
63
+
64
+ # Validates an Israeli bank account number.
65
+ #
66
+ # @param value [String] The bank account to validate
67
+ # @param format [Symbol] Format: :domestic, :iban, or :any
68
+ # @return [Boolean] true if valid, false otherwise
69
+ #
70
+ # @example
71
+ # Israeli.valid_bank_account?("4985622815429") # => true (domestic)
72
+ # Israeli.valid_bank_account?("IL620108000000099999999") # => true (IBAN)
73
+ def valid_bank_account?(value, format: :any)
74
+ Validators::BankAccount.valid?(value, format: format)
75
+ end
76
+
77
+ # Formats an Israeli ID number to standard 9-digit format.
78
+ #
79
+ # @param value [String, Integer] The ID number to format
80
+ # @return [String, nil] Formatted ID or nil if invalid
81
+ def format_id(value)
82
+ Validators::Id.format(value)
83
+ end
84
+
85
+ # Formats an Israeli phone number.
86
+ #
87
+ # @param value [String] The phone number to format
88
+ # @param style [Symbol] Format style: :dashed, :international, or :compact
89
+ # @return [String, nil] Formatted phone or nil if invalid
90
+ def format_phone(value, style: :dashed)
91
+ Validators::Phone.format(value, style: style)
92
+ end
93
+ end
94
+ end
95
+
96
+ # Require validators after module definition to avoid circular dependency
97
+ require_relative "israeli/validators/id"
98
+ require_relative "israeli/validators/postal_code"
99
+ require_relative "israeli/validators/phone"
100
+ require_relative "israeli/validators/bank_account"
101
+
102
+ # Load Rails integration if Rails is available
103
+ require_relative "israeli/railtie" if defined?(Rails::Railtie)
data/sig/israeli.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Israeli
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: israeli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - dpaluy
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Israeli provides validators for Israeli identifiers including Mispar
13
+ Zehut (ID numbers), phone numbers (mobile, landline, VoIP), postal codes, and bank
14
+ accounts (domestic and IBAN). Includes both standalone validators and ActiveModel/Rails
15
+ integration.
16
+ email:
17
+ - dpaluy@users.noreply.github.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files:
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ files:
25
+ - CHANGELOG.md
26
+ - LICENSE.txt
27
+ - README.md
28
+ - Rakefile
29
+ - lib/israeli.rb
30
+ - lib/israeli/active_model/israeli_bank_account_validator.rb
31
+ - lib/israeli/active_model/israeli_id_validator.rb
32
+ - lib/israeli/active_model/israeli_phone_validator.rb
33
+ - lib/israeli/active_model/israeli_postal_code_validator.rb
34
+ - lib/israeli/errors.rb
35
+ - lib/israeli/luhn.rb
36
+ - lib/israeli/railtie.rb
37
+ - lib/israeli/sanitizer.rb
38
+ - lib/israeli/validators/bank_account.rb
39
+ - lib/israeli/validators/id.rb
40
+ - lib/israeli/validators/phone.rb
41
+ - lib/israeli/validators/postal_code.rb
42
+ - lib/israeli/version.rb
43
+ - sig/israeli.rbs
44
+ homepage: https://github.com/dpaluy/israeli
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ rubygems_mfa_required: 'true'
49
+ homepage_uri: https://github.com/dpaluy/israeli
50
+ documentation_uri: https://rubydoc.info/gems/israeli
51
+ source_code_uri: https://github.com/dpaluy/israeli
52
+ changelog_uri: https://github.com/dpaluy/israeli/blob/main/CHANGELOG.md
53
+ bug_tracker_uri: https://github.com/dpaluy/israeli/issues
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.2.0
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 4.0.0
69
+ specification_version: 4
70
+ summary: Validation utilities for Israeli identifiers (ID, phone, postal code, bank
71
+ account).
72
+ test_files: []