swedish-pin 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ before_script:
3
+ bundle install
4
+ rvm:
5
+ - 2.5
6
+ - 2.6
7
+ - 2.7
8
+ sudo: false
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
@@ -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.
@@ -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.
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "swedish_pin"
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwedishPIN
4
+ # The version number of this library.
5
+ VERSION = "1.0.0"
6
+ 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: []