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.
@@ -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: []