swedish-pin 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 +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +47 -0
- data/LICENSE +21 -0
- data/README.md +60 -0
- data/Rakefile +10 -0
- data/lib/swedish-pin.rb +3 -0
- data/lib/swedish_pin.rb +107 -0
- data/lib/swedish_pin/errors.rb +40 -0
- data/lib/swedish_pin/generator.rb +46 -0
- data/lib/swedish_pin/parser.rb +151 -0
- data/lib/swedish_pin/personnummer.rb +205 -0
- data/lib/swedish_pin/version.rb +6 -0
- data/personnummer.gemspec +28 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bb6a746d0ef33cae631f59b11006ee17a98cc38af65b69b35a0f025a22beeae1
|
4
|
+
data.tar.gz: c9e0ca8008e77aab92808afee2e619bf6a7ed8d9461319449a304128a50237d5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cd9f5e5328d76e4d709a4d0b8e688b3e13fd186fb57c66eb82a8793215e01231cb1799d41a4e16a75b306d1d37c9cb3d7a2c1cb98a0ae79dec18ea5bc9cf554e
|
7
|
+
data.tar.gz: 4b7ed29721a100dc98515d181748f47d0c3957d677a19fb93a6ee287837a270dd845ccdaf20ec504735d7e20ad4d03629dc48cdb052d112380fafb3315ced81a
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,15 @@
|
|
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.0.0/),
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
|
+
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
## [1.0.0] - 2020-09-09
|
11
|
+
|
12
|
+
Initial release.
|
13
|
+
|
14
|
+
[Unreleased]: https://github.com/Mange/swedish-pin-ruby/compare/v1.0.0...HEAD
|
15
|
+
[1.0.0]: https://github.com/Mange/swedish-pin-ruby/releases/tag/v1.0.0
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
swedish-pin (1.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.1)
|
10
|
+
minitest (5.14.2)
|
11
|
+
parallel (1.19.2)
|
12
|
+
parser (2.7.1.4)
|
13
|
+
ast (~> 2.4.1)
|
14
|
+
rainbow (3.0.0)
|
15
|
+
rake (13.0.1)
|
16
|
+
regexp_parser (1.7.1)
|
17
|
+
rexml (3.2.4)
|
18
|
+
rubocop (0.90.0)
|
19
|
+
parallel (~> 1.10)
|
20
|
+
parser (>= 2.7.1.1)
|
21
|
+
rainbow (>= 2.2.2, < 4.0)
|
22
|
+
regexp_parser (>= 1.7)
|
23
|
+
rexml
|
24
|
+
rubocop-ast (>= 0.3.0, < 1.0)
|
25
|
+
ruby-progressbar (~> 1.7)
|
26
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
27
|
+
rubocop-ast (0.3.0)
|
28
|
+
parser (>= 2.7.1.4)
|
29
|
+
rubocop-performance (1.8.0)
|
30
|
+
rubocop (>= 0.87.0)
|
31
|
+
ruby-progressbar (1.10.1)
|
32
|
+
standard (0.6.0)
|
33
|
+
rubocop (~> 0.90)
|
34
|
+
rubocop-performance (~> 1.8.0)
|
35
|
+
unicode-display_width (1.7.0)
|
36
|
+
|
37
|
+
PLATFORMS
|
38
|
+
ruby
|
39
|
+
|
40
|
+
DEPENDENCIES
|
41
|
+
minitest
|
42
|
+
rake
|
43
|
+
standard
|
44
|
+
swedish-pin!
|
45
|
+
|
46
|
+
BUNDLED WITH
|
47
|
+
2.1.4
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2017-2020 - Personnummer, Magnus Bergmark and Contributors
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# swedish-pin
|
2
|
+
|
3
|
+
[](http://travis-ci.org/Mange/swedish-pin-ruby) [](http://inch-ci.org/github/Mange/swedish-pin-ruby)
|
4
|
+
|
5
|
+
Validate, parse, and generate [Swedish Personal Identity
|
6
|
+
Numbers](https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden))
|
7
|
+
("PINs", or *Personnummer*).
|
8
|
+
|
9
|
+
[API documentation](https://www.rubydoc.info/gems/swedish-pin)
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this to your `Gemfile`
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'swedish-pin'
|
17
|
+
```
|
18
|
+
|
19
|
+
Then run `bundle install`.
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require "swedish_pin"
|
25
|
+
|
26
|
+
# Validate strings
|
27
|
+
SwedishPIN.valid?("8507099805") # => true
|
28
|
+
SwedishPIN.valid?("8507099804") # => false
|
29
|
+
|
30
|
+
# Parse numbers to get more information about them, or to normalize display of
|
31
|
+
# them.
|
32
|
+
pin = SwedishPIN.parse("8507099805") # => #<SwedishPIN::Personnummer …>
|
33
|
+
pin.year # => 1985
|
34
|
+
pin.birthdate # => #<Date: 1985-07-09>
|
35
|
+
|
36
|
+
# The 10-digit variant also knows about century separators.
|
37
|
+
pin.to_s # => "850709-9805"
|
38
|
+
pin.to_s(10) # => "850709-9805"
|
39
|
+
pin.format_short(Date.civil(2025, 12, 1)) # => "850709-9805"
|
40
|
+
pin.format_short(Date.civil(2085, 12, 1)) # => "850709+9805"
|
41
|
+
|
42
|
+
# Use unofficial 12-digit format for a stable string that doesn't change
|
43
|
+
# depending on today's date when storing it.
|
44
|
+
pin.to_s(12) # => "19850709-9805"
|
45
|
+
pin.format_long # => "19850709-9805"
|
46
|
+
|
47
|
+
# You can also generate numbers to use as example data
|
48
|
+
fake1 = SwedishPIN.generate
|
49
|
+
fake2 = SwedishPIN.generate(user.birthday)
|
50
|
+
```
|
51
|
+
|
52
|
+
## License
|
53
|
+
|
54
|
+
MIT. See `LICENSE` file for more details.
|
55
|
+
|
56
|
+
This project started out as a fork of
|
57
|
+
[personnummer/ruby](https://github.com/personnummer/ruby), but has since been
|
58
|
+
almost completely rewritten.
|
59
|
+
Despite this, the original authors retains most of the copyright since this is
|
60
|
+
derivative work.
|
data/Rakefile
ADDED
data/lib/swedish-pin.rb
ADDED
data/lib/swedish_pin.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
# Validate, parse, and generate Swedish Personal Identity Numbers.
|
4
|
+
#
|
5
|
+
# In Swedish these are called _Personnummer_. There is also a variant called
|
6
|
+
# "coordination number" (_Samordningsnummer_). Both of these are supported
|
7
|
+
# using the same API; see {SwedishPIN::Personnummer#coordination_number?}.
|
8
|
+
#
|
9
|
+
# To get started, look at {SwedishPIN.valid?} and {SwedishPIN.parse}.
|
10
|
+
module SwedishPIN
|
11
|
+
autoload :Generator, "swedish_pin/generator"
|
12
|
+
autoload :Parser, "swedish_pin/parser"
|
13
|
+
autoload :Personnummer, "swedish_pin/personnummer"
|
14
|
+
autoload :VERSION, "swedish_pin/version"
|
15
|
+
|
16
|
+
autoload :ParseError, "swedish_pin/errors"
|
17
|
+
autoload :InvalidFormat, "swedish_pin/errors"
|
18
|
+
autoload :InvalidDate, "swedish_pin/errors"
|
19
|
+
autoload :InvalidChecksum, "swedish_pin/errors"
|
20
|
+
|
21
|
+
# Parses a string of a personnummer and returns a
|
22
|
+
# {SwedishPIN::Personnummer} or raises an error.
|
23
|
+
#
|
24
|
+
# Some numbers will have to relate to the current time in order to be parsed
|
25
|
+
# correctly. For example, the PIN +201231-…+ could be in many different
|
26
|
+
# years, including 1820, 1920, 2020, and so on.
|
27
|
+
# This library will guess that the year is in the most recent guess that are
|
28
|
+
# in the past. So during the year 2020 it would guess 2020, and in 2019 it
|
29
|
+
# will guess 1920.
|
30
|
+
#
|
31
|
+
# @param [String] string The number to parse.
|
32
|
+
# @param [Time] now Provide a different "parse time" context.
|
33
|
+
# @return [SwedishPIN::Personnummer] The parsed PIN
|
34
|
+
# @raise {SwedishPIN::ParseError} When the provided string was not valid.
|
35
|
+
# @raise {ArgumentError} When the provided value was not a +String+.
|
36
|
+
def self.parse(string, now = Time.now)
|
37
|
+
result = Parser.new(string, now).parse
|
38
|
+
Personnummer.new(
|
39
|
+
year: result.fetch(:year),
|
40
|
+
month: result.fetch(:month),
|
41
|
+
day: result.fetch(:day),
|
42
|
+
sequence_number: result.fetch(:sequence_number),
|
43
|
+
control_digit: result.fetch(:control_digit)
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Checks if a provided string is a valid _Personnummer_.
|
48
|
+
#
|
49
|
+
# @param [String] string The number to parse.
|
50
|
+
# @param [Time] now Provide a different "parse time" context. See {.parse}.
|
51
|
+
# @return [true, false] if the string was valid
|
52
|
+
def self.valid?(string, now = Time.now)
|
53
|
+
Parser.new(string, now).valid?
|
54
|
+
rescue ArgumentError
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
# Generates a valid _Personnummer_ given certain inputs. Inputs not provided
|
59
|
+
# will be randomized.
|
60
|
+
#
|
61
|
+
# This is mainly useful in order to generate test data or "Lorem Ipsum"-like
|
62
|
+
# values for use in demonstrations. Note that valid PINs might actually
|
63
|
+
# correspond to a real person, so don't use these generated PINs for anything
|
64
|
+
# that has a real effect.
|
65
|
+
#
|
66
|
+
# @example FactoryBot sequence
|
67
|
+
# FactoryBot.define do
|
68
|
+
# sequence(:swedish_pin) do |n|
|
69
|
+
# # Will generate every PIN for a full day, then flip over to the next
|
70
|
+
# # day and start the sequence over.
|
71
|
+
# sequence_number = n % 1000
|
72
|
+
# date = Date.civil(1950, 1, 1) + (n / 1000)
|
73
|
+
# SwedishPIN.generate(date, sequence_number)
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# @example Test data
|
78
|
+
# user = User.new(name: "Jane Doe", pin: SwedishPIN.generate)
|
79
|
+
#
|
80
|
+
# @raise [ArgumentError] if given numbers are outside of the valid range.
|
81
|
+
# @param [Date, Time, nil] birthday The birthday of the person the PIN identifies, or +nil+ for a random date in the past.
|
82
|
+
# @param [String, Integer, nil] sequence_number The sequence number that correspond to the three digits after the birthday, or +nil+ to pick a random one.
|
83
|
+
def self.generate(birthday = nil, sequence_number = nil)
|
84
|
+
Generator.new(birthday).generate(sequence_number)
|
85
|
+
end
|
86
|
+
|
87
|
+
# @api private
|
88
|
+
#
|
89
|
+
# Implementation of Luhn algorithm.
|
90
|
+
#
|
91
|
+
# @param [String] digits String of digits to calculate a control digit for.
|
92
|
+
# @return [Integer] Control digit.
|
93
|
+
def self.luhn(digits)
|
94
|
+
sum = 0
|
95
|
+
|
96
|
+
(0...digits.length).each do |i|
|
97
|
+
v = digits[i].to_i
|
98
|
+
v *= 2 - (i % 2)
|
99
|
+
if v > 9
|
100
|
+
v -= 9
|
101
|
+
end
|
102
|
+
sum += v
|
103
|
+
end
|
104
|
+
|
105
|
+
((sum.to_f / 10).ceil * 10 - sum.to_f).to_i
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwedishPIN
|
4
|
+
# Error that represents a problem that occurred while parsing a
|
5
|
+
# _Personnummer_.
|
6
|
+
#
|
7
|
+
# You can inspect both the {input} attribute and check the subclass to build
|
8
|
+
# more helpful error messages for your users.
|
9
|
+
class ParseError < RuntimeError
|
10
|
+
# The input string that caused the error.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# error.input # => "112233@4455"
|
14
|
+
attr_reader :input
|
15
|
+
|
16
|
+
# Create a new instance of this error.
|
17
|
+
#
|
18
|
+
# @param [String] message The error message.
|
19
|
+
# @param [String] input The input string that could not be parsed.
|
20
|
+
def initialize(message, input)
|
21
|
+
super(message)
|
22
|
+
@input = input
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# The format of the input string does not look like a _Personnummer_. This
|
27
|
+
# could happen because the string has the wrong length, or because extra
|
28
|
+
# characters are placed inside of it.
|
29
|
+
class InvalidFormat < ParseError
|
30
|
+
end
|
31
|
+
|
32
|
+
# The date embedded in the input string is not a valid date.
|
33
|
+
class InvalidDate < ParseError
|
34
|
+
end
|
35
|
+
|
36
|
+
# The control digit at the end does not match the rest of the input. This
|
37
|
+
# could mean that the input has a typo.
|
38
|
+
class InvalidChecksum < ParseError
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module SwedishPIN
|
6
|
+
# @private
|
7
|
+
# @api private
|
8
|
+
#
|
9
|
+
# Generator for PINs.
|
10
|
+
class Generator
|
11
|
+
# The date all generated PINs will be based on.
|
12
|
+
attr_reader :date
|
13
|
+
|
14
|
+
# Creates a new generator for a particular date.
|
15
|
+
def initialize(date)
|
16
|
+
@date = date || random_date
|
17
|
+
end
|
18
|
+
|
19
|
+
# Generate a {Personnummer} with the given sequence number.
|
20
|
+
def generate(sequence_number)
|
21
|
+
sequence_number ||= random_sequence_number
|
22
|
+
Personnummer.new(
|
23
|
+
year: date.year,
|
24
|
+
month: date.month,
|
25
|
+
day: date.day,
|
26
|
+
sequence_number: sequence_number,
|
27
|
+
control_digit: control_digit(sequence_number)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def random_date
|
34
|
+
Date.today - Random.rand(0..(110 * 365))
|
35
|
+
end
|
36
|
+
|
37
|
+
def random_sequence_number
|
38
|
+
Random.rand(0..999)
|
39
|
+
end
|
40
|
+
|
41
|
+
def control_digit(sequence_number)
|
42
|
+
padded = ("%03d" % sequence_number)
|
43
|
+
SwedishPIN.luhn("#{date.strftime("%y%m%d")}#{padded}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwedishPIN
|
4
|
+
# @api private
|
5
|
+
#
|
6
|
+
# Parser for _Personnummer_.
|
7
|
+
#
|
8
|
+
# Please use {SwedishPIN.parse} or {SwedishPIN.valid?} instead.
|
9
|
+
class Parser
|
10
|
+
MATCHER = /
|
11
|
+
\A
|
12
|
+
(?<century>\d{2})?
|
13
|
+
(?<year>\d{2})
|
14
|
+
(?<month>\d{2})
|
15
|
+
(?<day>\d{2})
|
16
|
+
(?<separator>[+\- ]?)
|
17
|
+
(?<sequence_number>\d{3})
|
18
|
+
(?<control_digit>\d)
|
19
|
+
\z
|
20
|
+
/x.freeze
|
21
|
+
private_constant :MATCHER
|
22
|
+
|
23
|
+
# Setup a new parser.
|
24
|
+
def initialize(input, now = Time.now)
|
25
|
+
unless input.is_a?(String)
|
26
|
+
raise ArgumentError, "Expected String, got #{input.inspect}"
|
27
|
+
end
|
28
|
+
|
29
|
+
@input = input
|
30
|
+
@now = now
|
31
|
+
@matches = MATCHER.match(input.strip)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Raise {ParseError} if anything in the input isn't valid.
|
35
|
+
def validate
|
36
|
+
validate_match
|
37
|
+
validate_luhn
|
38
|
+
validate_date
|
39
|
+
end
|
40
|
+
|
41
|
+
# Check validity without raising.
|
42
|
+
def valid?
|
43
|
+
validate
|
44
|
+
true
|
45
|
+
rescue
|
46
|
+
false
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return +Hash+ of parsed values to be used with {SwedishPIN::Personnummer#initialize}.
|
50
|
+
def parse
|
51
|
+
validate
|
52
|
+
|
53
|
+
{
|
54
|
+
year: full_year,
|
55
|
+
month: month,
|
56
|
+
day: day,
|
57
|
+
sequence_number: sequence_number,
|
58
|
+
control_digit: control_digit
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
attr_reader :now
|
65
|
+
|
66
|
+
def full_year
|
67
|
+
century * 100 + year
|
68
|
+
end
|
69
|
+
|
70
|
+
def century
|
71
|
+
if @matches["century"]
|
72
|
+
Integer(@matches["century"], 10)
|
73
|
+
else
|
74
|
+
guess_century
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def year
|
79
|
+
Integer(@matches["year"], 10)
|
80
|
+
end
|
81
|
+
|
82
|
+
def month
|
83
|
+
@month ||= Integer(@matches["month"], 10)
|
84
|
+
end
|
85
|
+
|
86
|
+
def day
|
87
|
+
@day ||= Integer(@matches["day"], 10)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Day, but adjusted for coordination numbers being possible.
|
91
|
+
def real_day
|
92
|
+
if day > 60
|
93
|
+
day - 60
|
94
|
+
else
|
95
|
+
day
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def sequence_number
|
100
|
+
Integer(@matches["sequence_number"], 10)
|
101
|
+
end
|
102
|
+
|
103
|
+
def control_digit
|
104
|
+
Integer(@matches["control_digit"], 10)
|
105
|
+
end
|
106
|
+
|
107
|
+
def guess_century
|
108
|
+
guessed_year = (now.year / 100) * 100 + year
|
109
|
+
|
110
|
+
# Don't guess future dates; skip back a century when that happens.
|
111
|
+
if Time.new(guessed_year, month, real_day) > now
|
112
|
+
guessed_year -= 100
|
113
|
+
end
|
114
|
+
|
115
|
+
# The "+" separator means another century back.
|
116
|
+
if @matches["separator"] == "+"
|
117
|
+
guessed_year -= 100
|
118
|
+
end
|
119
|
+
|
120
|
+
guessed_year / 100
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_match
|
124
|
+
unless @matches
|
125
|
+
raise InvalidFormat.new("Input did not match expected format", @input)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def validate_luhn
|
130
|
+
comparator = [
|
131
|
+
@matches["year"],
|
132
|
+
@matches["month"],
|
133
|
+
@matches["day"],
|
134
|
+
@matches["sequence_number"]
|
135
|
+
].join("")
|
136
|
+
|
137
|
+
if SwedishPIN.luhn(comparator) != control_digit
|
138
|
+
raise InvalidChecksum.new("Control digit did not match expected value", @input)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def validate_date
|
143
|
+
raise InvalidDate.new("#{month} is not a valid month", @input) unless (1..12).cover?(month)
|
144
|
+
raise InvalidDate.new("#{day} is not a valid day", @input) unless (1..31).cover?(real_day)
|
145
|
+
|
146
|
+
unless Date.valid_date?(full_year, month, real_day)
|
147
|
+
raise InvalidDate.new("Input had invalid date", @input)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwedishPIN
|
4
|
+
# Represents a parsed and valid _Personnummer_ or _Samordningsnummer_ for a
|
5
|
+
# particular individual.
|
6
|
+
#
|
7
|
+
# Determine if this is a _Personnummer_ or a _Samordningsnummer_ using {coordination_number?}.
|
8
|
+
#
|
9
|
+
# @see https://en.wikipedia.org/wiki/Personal_identity_number_(Sweden) Personnummer on Wikipedia.
|
10
|
+
class Personnummer
|
11
|
+
attr_reader :year, :month, :day, :sequence_number, :control_digit
|
12
|
+
# @!attribute [r] year
|
13
|
+
# The full year of the _personnummer_. For example +1989+.
|
14
|
+
# @return [Integer]
|
15
|
+
# @!attribute [r] month
|
16
|
+
# The month digit of the _personnummer_. 1 for January up until 12 for December.
|
17
|
+
# @return [Integer]
|
18
|
+
# @!attribute [r] day
|
19
|
+
# The day of the month of the _personnummer_. This will be for the real
|
20
|
+
# day, even for coordination numbers.
|
21
|
+
# @see #coordination_number?
|
22
|
+
# @return [Integer]
|
23
|
+
# @!attribute [r] sequence_number
|
24
|
+
# The number after the separator.
|
25
|
+
# A common reason to access this is to check the sex of the person. You
|
26
|
+
# might want to look at {#male?} and {#female?} instead.
|
27
|
+
# @note This attribute returns an +Integer+, but this sequence needs to
|
28
|
+
# be zero-padded up to three characters if you intend to display it (i.e.
|
29
|
+
# +3+ is +"003"+).
|
30
|
+
# @return [Integer]
|
31
|
+
# @!attribute [r] control_digit
|
32
|
+
# The last digit of the _personnummer_. It acts as a checksum of the
|
33
|
+
# previous numbers.
|
34
|
+
# @return [Integer]
|
35
|
+
|
36
|
+
# @api private
|
37
|
+
# @private
|
38
|
+
#
|
39
|
+
# Initializes a new instance from specific values. Please consider using
|
40
|
+
# {SwedishPIN.generate} instead of you want custom instances.
|
41
|
+
def initialize(year:, month:, day:, sequence_number:, control_digit:)
|
42
|
+
@year = year
|
43
|
+
@month = month
|
44
|
+
@coordination_number = day > 60
|
45
|
+
@day = (day > 60 ? day - 60 : day)
|
46
|
+
@sequence_number = sequence_number
|
47
|
+
@control_digit = control_digit
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return the birthday for the person that is represented by this
|
51
|
+
# _Personnummer_.
|
52
|
+
#
|
53
|
+
# @return [Date] the date of birth
|
54
|
+
def birthday
|
55
|
+
Date.civil(year, month, day)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns +true+ if this number is a _Samordningsnummer_ (coordination
|
59
|
+
# number). This is a number that is granted to non-Swedish citizens until
|
60
|
+
# the time that they become citizens.
|
61
|
+
#
|
62
|
+
# Coordination numbers are identical to a PIN, except that the "day"
|
63
|
+
# component has +60+ added to it (i.e. <code>28+60=88</code>).
|
64
|
+
#
|
65
|
+
# @note The {day} attribute will still return a valid date day, even for coordination numbers.
|
66
|
+
# @see https://sv.wikipedia.org/wiki/Samordningsnummer Samordningsnummer on Wikipedia (Swedish)
|
67
|
+
def coordination_number?
|
68
|
+
@coordination_number
|
69
|
+
end
|
70
|
+
|
71
|
+
# Formats the PIN in the official "10-digit" format. This is the "real"
|
72
|
+
# _Personnummer_ string.
|
73
|
+
#
|
74
|
+
# *Format:* +yymmdd-nnnn+ or <code>yymmdd+nnnn</code>
|
75
|
+
#
|
76
|
+
# The _Personnummer_ specification says that starting from the year of a
|
77
|
+
# person's 100th birthday, the separator in their _personnummer_ will
|
78
|
+
# change from a <code>-</code> into a <code>+</code>.
|
79
|
+
#
|
80
|
+
# That means that every time you display a _personnummer_ you also must
|
81
|
+
# consider the time of this action. Something that was read on date A and
|
82
|
+
# outputted on date B might not use the same string representation.
|
83
|
+
#
|
84
|
+
# For this reason, the real _personnummer_ is usually not what you want to
|
85
|
+
# store, only what you want to display in some cases.
|
86
|
+
#
|
87
|
+
# This library recommends that you use {format_long} for storage.
|
88
|
+
#
|
89
|
+
# @param [Time, Date] now The time when this personnummer is supposed to be displayed.
|
90
|
+
# @return [String] the formatted number
|
91
|
+
# @see #format_long
|
92
|
+
def format_short(now = Time.now)
|
93
|
+
[
|
94
|
+
format_date(false),
|
95
|
+
short_separator(now),
|
96
|
+
"%03d" % sequence_number,
|
97
|
+
control_digit
|
98
|
+
].join("")
|
99
|
+
end
|
100
|
+
|
101
|
+
# Formats the _personnummer_ in the unofficial "12-digit" format that
|
102
|
+
# includes the century and doesn't change separator depending on when the
|
103
|
+
# number is supposed to be shown.
|
104
|
+
#
|
105
|
+
# This format is being adopted in a lot of places in favor of the
|
106
|
+
# "10-digit" format ({format_short}), but as of 2020 it remains an
|
107
|
+
# unofficial format.
|
108
|
+
#
|
109
|
+
# *Format:* +yyyymmdd-nnnn+
|
110
|
+
#
|
111
|
+
# @see #format_short
|
112
|
+
def format_long
|
113
|
+
[
|
114
|
+
format_date(true),
|
115
|
+
"-",
|
116
|
+
"%03d" % sequence_number,
|
117
|
+
control_digit
|
118
|
+
].join("")
|
119
|
+
end
|
120
|
+
|
121
|
+
# Formats the PIN into a +String+.
|
122
|
+
#
|
123
|
+
# You can provide the desired length to get different formats.
|
124
|
+
#
|
125
|
+
# @note The length isn't how long the resulting string will be as the
|
126
|
+
# resulting string will also have a separator included. The formats are
|
127
|
+
# colloquially called "10-digit" and "12-digit", which is why they are
|
128
|
+
# referred to as "length" here.
|
129
|
+
#
|
130
|
+
# +10+ or +nil+:: {format_short}
|
131
|
+
# +12+:: {format_long}
|
132
|
+
#
|
133
|
+
# @param [Integer, nil] length The desired format.
|
134
|
+
# @param [Time, Date] now The current time. Only used by {format_short}.
|
135
|
+
# @raise [ArgumentError] If not provided a valid length.
|
136
|
+
# @return [String]
|
137
|
+
# @see #format_short
|
138
|
+
# @see #format_long
|
139
|
+
def to_s(length = 10, now = Time.now)
|
140
|
+
case length
|
141
|
+
when 10 then format_short(now)
|
142
|
+
when 12 then format_long
|
143
|
+
else raise ArgumentError, "The only supported lengths are 10 or 12."
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns the age of the person this _personnummer_ represents, as an
|
148
|
+
# integer of years since birth.
|
149
|
+
#
|
150
|
+
# Swedish age could be defined as such: A person will be +0+ years old when
|
151
|
+
# born, and +1+ 12 months after that, on the same day or the day after in
|
152
|
+
# the case of leap years. This is the same way most western countries count
|
153
|
+
# age.
|
154
|
+
#
|
155
|
+
# If the {birthday} is in the future, then +0+ will be returned.
|
156
|
+
#
|
157
|
+
# @param [Time, Date] now The current time.
|
158
|
+
# @return [Integer] Number of 12 month periods that have passed since the birthdate; +0+ or more.
|
159
|
+
def age(now = Time.now)
|
160
|
+
age = now.year - year - (birthday_passed_this_year?(now) ? 0 : 1)
|
161
|
+
[0, age].max
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns +true+ if the _personnummer_ represents a person that is legally
|
165
|
+
# identified as +male+.
|
166
|
+
# @return [true, false]
|
167
|
+
def male?
|
168
|
+
sequence_number.odd?
|
169
|
+
end
|
170
|
+
|
171
|
+
# Returns +true+ if the _personnummer_ represents a person that is legally
|
172
|
+
# identified as +female+.
|
173
|
+
# @return [true, false]
|
174
|
+
def female?
|
175
|
+
sequence_number.even?
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def short_separator(now)
|
181
|
+
if year <= (now.year - 100)
|
182
|
+
"+"
|
183
|
+
else
|
184
|
+
"-"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def format_date(include_century)
|
189
|
+
[
|
190
|
+
(include_century ? pad(year / 100) : nil),
|
191
|
+
pad(year % 100),
|
192
|
+
pad(month),
|
193
|
+
pad(coordination_number? ? day + 60 : day)
|
194
|
+
].join("")
|
195
|
+
end
|
196
|
+
|
197
|
+
def pad(num)
|
198
|
+
"%02d" % num
|
199
|
+
end
|
200
|
+
|
201
|
+
def birthday_passed_this_year?(now)
|
202
|
+
now.month > month || (now.month == month && now.day >= day)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require_relative "lib/swedish_pin/version"
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "swedish-pin"
|
5
|
+
s.version = SwedishPIN::VERSION
|
6
|
+
s.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
7
|
+
|
8
|
+
s.license = "MIT"
|
9
|
+
s.summary = "Work with Swedish PINs (Personnummer)"
|
10
|
+
s.description = "Parse, validate, and generate Swedish Personal Identity Numbers (PINs / Personnummer)"
|
11
|
+
s.homepage = "https://github.com/Mange/swedish-pin-ruby"
|
12
|
+
|
13
|
+
s.authors = ["Magnus Bergmark", "Jack Millard", "Fredrik Forsmo"]
|
14
|
+
s.email = ["me@mange.dev", "millard64@hotmail.co.uk", "fredrik.forsmo@gmail.com"]
|
15
|
+
|
16
|
+
# Specify which files should be added to the gem when it is released.
|
17
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
18
|
+
s.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
end
|
21
|
+
s.bindir = "exe"
|
22
|
+
s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
|
25
|
+
s.add_development_dependency "rake"
|
26
|
+
s.add_development_dependency "minitest"
|
27
|
+
s.add_development_dependency "standard"
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: swedish-pin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Magnus Bergmark
|
8
|
+
- Jack Millard
|
9
|
+
- Fredrik Forsmo
|
10
|
+
autorequire:
|
11
|
+
bindir: exe
|
12
|
+
cert_chain: []
|
13
|
+
date: 2020-09-09 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rake
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ">="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
version: '0'
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: minitest
|
31
|
+
requirement: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: standard
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
description: Parse, validate, and generate Swedish Personal Identity Numbers (PINs
|
58
|
+
/ Personnummer)
|
59
|
+
email:
|
60
|
+
- me@mange.dev
|
61
|
+
- millard64@hotmail.co.uk
|
62
|
+
- fredrik.forsmo@gmail.com
|
63
|
+
executables: []
|
64
|
+
extensions: []
|
65
|
+
extra_rdoc_files: []
|
66
|
+
files:
|
67
|
+
- ".gitignore"
|
68
|
+
- ".travis.yml"
|
69
|
+
- CHANGELOG.md
|
70
|
+
- Gemfile
|
71
|
+
- Gemfile.lock
|
72
|
+
- LICENSE
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- lib/swedish-pin.rb
|
76
|
+
- lib/swedish_pin.rb
|
77
|
+
- lib/swedish_pin/errors.rb
|
78
|
+
- lib/swedish_pin/generator.rb
|
79
|
+
- lib/swedish_pin/parser.rb
|
80
|
+
- lib/swedish_pin/personnummer.rb
|
81
|
+
- lib/swedish_pin/version.rb
|
82
|
+
- personnummer.gemspec
|
83
|
+
homepage: https://github.com/Mange/swedish-pin-ruby
|
84
|
+
licenses:
|
85
|
+
- MIT
|
86
|
+
metadata: {}
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: 2.5.0
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubygems_version: 3.0.8
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: Work with Swedish PINs (Personnummer)
|
106
|
+
test_files: []
|