diff --git a/doc/unreleased/driving_licence.md b/doc/unreleased/driving_licence.md new file mode 100644 index 0000000000..7e21baeecd --- /dev/null +++ b/doc/unreleased/driving_licence.md @@ -0,0 +1,21 @@ +# Faker::DrivingLicence + +```ruby +# Generate a licence number in GB format, as issued in England, Scotland and Wales +# The DVSA does not publish their checksum algorithm, so the last 3 characters +# are random +# Optional arguments: last_name, initials, date_of_birth, gender +Faker::DrivingLicence.british_driving_licence #=> "MCDER712081VF7EK" +Faker::DrivingLicence.british_driving_licence(last_name: "O'Carroll", + initials: "J", + gender: :female, + date_of_birth: Date.parse("1986-10-24")) #=> "OCARR815246J91HT" + +# Generate a Northern Irish licence number +Faker::DrivingLicence.northern_irish_driving_licence #=> "70702548" + +# Generate a UK driving licence number in either GB or NI format, at a rate +# consistent with their relative populations +# Optional arguments: last_name, initials, date_of_birth, gender +Faker::DrivingLicence.uk_driving_licence #=> "OCARR815246J91HT" +Faker::DrivingLicence.uk_driving_licence #=> "70702548" diff --git a/lib/faker/driving_licence.rb b/lib/faker/driving_licence.rb new file mode 100644 index 0000000000..6fd3a52e1e --- /dev/null +++ b/lib/faker/driving_licence.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Faker + class DrivingLicence < Base + GB_PADDING = '9999' + NI_CHANCE = 0.03 # NI Pop is about 3% of total UK population + + class << self + def british_driving_licence(last_name: Faker::Name.last_name, + initials: Faker::Name.initials, + gender: random_gender, + date_of_birth: Faker::Date.birthday(18, 65)) + [ + gb_licence_padding(last_name, 5), + gb_licence_year(date_of_birth, gender), + gb_licence_padding(initials, 2), + gb_licence_checksum + ].join + end + + def northern_irish_driving_licence + Faker::Number.number(8) + end + + def uk_driving_licence(*args) + if Faker::Config.random.rand < NI_CHANCE + northern_irish_driving_licence + else + british_driving_licence(*args) + end + end + + private + + def random_gender + %i[male female].sample(random: Faker::Config.random) + end + + def gb_licence_padding(str, num_chars) + prepped = str.upcase.gsub(%r{[^A-Z]}, '') + GB_PADDING + prepped[0..(num_chars - 1)] + end + + def gb_licence_year(dob, gender) + decade = (dob.year / 10) % 10 + year = dob.year % 10 + month = gender == :female ? dob.month + 5 : dob.month + # Rubocop's preferred formatting is pretty gory + # rubocop:disable FormatString + "#{decade}#{'%02d' % month}#{'%02d' % dob.day}#{year}" + # rubocop:enable FormatString + end + + def gb_licence_checksum + regexify(/[0-9][A-Z][A-Z]/) + end + end + end +end diff --git a/test/test_faker_driving_licence.rb b/test/test_faker_driving_licence.rb new file mode 100644 index 0000000000..d113b84ddd --- /dev/null +++ b/test/test_faker_driving_licence.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require_relative 'test_helper' + +class TestFakerDrivingLicence < Test::Unit::TestCase + def setup + @tester = Faker::DrivingLicence + end + + def test_valid_gb_licence + sample = @tester.british_driving_licence + # GB Licence number is 16 characters long + assert_equal 16, sample.length + # First 5 characters are the last_name, right-padded with '9' + assert_match %r{[A-Z][A-Z9]{4}}, sample[0..4] + # Next 6 are digits + assert_match %r{[0-9]{6}}, sample[5..10] + # comprising: + # Single digit decade of birth + assert_includes 0..9, sample[5].to_i + # 2 digit month of birth (add 5 if female) + assert_includes 1..17, sample[6..7].to_i + # 2 digit day of birth + assert_includes 1..31, sample[8..9].to_i + # and least significant digit of birth year + assert_includes 0..9, sample[10].to_i + # Next 2 are first 2 initials of forenames, padded with '9' + assert_match %r{[A-Z][A-Z9]}, sample[11..12] + # Last stanza is a tie-breaker digit + 2 letter checksum + assert_match %r{[0-9][A-Z0-9]{2}}, sample[13..15] + end + + def test_valid_northern_irish_licence + sample = @tester.northern_irish_driving_licence + # NI licence is an opaque 8-digit number + assert_equal 8, sample.length + assert_match %r{[0-9]{8}}, sample + end + + def test_uk_licence + sample = @tester.uk_driving_licence + assert_includes [8, 16], sample.length + end + + def test_british_licence_correctly_mangles_last_name + padded = @tester.british_driving_licence(last_name: 'Judd') + assert_equal 'JUDD9', padded[0..4] + truncated = @tester.british_driving_licence(last_name: 'Hamilton') + assert_match %r{HAMIL[0-9]}, truncated[0..5] + cleaned = @tester.british_driving_licence(last_name: "O'Carroll") + assert_equal 'OCARR', cleaned[0..4] + end + + def test_british_licence_correctly_mangles_date_of_birth + date_of_birth = Date.parse('1978-02-13') + male = @tester.british_driving_licence(date_of_birth: date_of_birth, gender: :male) + assert_equal '702138', male[5..10] + female = @tester.british_driving_licence(date_of_birth: date_of_birth, gender: :female) + assert_equal '707138', female[5..10] + end + + def test_british_licence_correctly_builds_initials + padded = @tester.british_driving_licence(initials: 'A') + assert_equal 'A9', padded[11..12] + truncated = @tester.british_driving_licence(initials: 'NLTC') + assert_equal 'NL', truncated[11..12] + end +end diff --git a/unreleased_CONTENT.md b/unreleased_CONTENT.md index 08a355b78f..0381884d79 100644 --- a/unreleased_CONTENT.md +++ b/unreleased_CONTENT.md @@ -52,6 +52,7 @@ Contents - [Faker::Demographic](doc/unreleased/demographic.md) - [Faker::Dessert](doc/unreleased/dessert.md) - [Faker::Device](doc/unreleased/device.md) + - [Faker::DrivingLicence](doc/unreleased/driving_licence.md) - [Faker::DrWho](doc/unreleased/dr_who.md) - [Faker::DumbAndDumber](doc/unreleased/dumb_and_dumber.md) - [Faker::Dune](doc/unreleased/dune.md)