swedish-pin 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://secure.travis-ci.org/Mange/swedish-pin-ruby.png?branch=master)](http://travis-ci.org/Mange/swedish-pin-ruby) [![Inline docs](http://inch-ci.org/github/Mange/swedish-pin-ruby.svg?branch=master)](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: []
|