Module: Philiprehberger::Phone

Defined in:
lib/philiprehberger/phone.rb,
lib/philiprehberger/phone/vanity.rb,
lib/philiprehberger/phone/carrier.rb,
lib/philiprehberger/phone/version.rb,
lib/philiprehberger/phone/area_code.rb,
lib/philiprehberger/phone/countries.rb,
lib/philiprehberger/phone/shortcode.rb,
lib/philiprehberger/phone/phone_type.rb

Defined Under Namespace

Modules: AreaCodeLookup, CarrierLookup, PhoneTypeDetection, ShortcodeValidation, VanityConversion Classes: ParseError, PhoneNumber

Constant Summary collapse

VANITY_MAP =
{
  'A' => '2', 'B' => '2', 'C' => '2',
  'D' => '3', 'E' => '3', 'F' => '3',
  'G' => '4', 'H' => '4', 'I' => '4',
  'J' => '5', 'K' => '5', 'L' => '5',
  'M' => '6', 'N' => '6', 'O' => '6',
  'P' => '7', 'Q' => '7', 'R' => '7', 'S' => '7',
  'T' => '8', 'U' => '8', 'V' => '8',
  'W' => '9', 'X' => '9', 'Y' => '9', 'Z' => '9'
}.freeze
CARRIER_PREFIXES =

Carrier identification based on prefix ranges These are representative ranges; real carrier data changes frequently

{
  us: {
    'AT&T' => %w[
      200 201 202 203 205 206 207 208 209 210
      212 213 214 215 217 219 224 225 228 229
      231 234 239 240 248 251 252 253 254 256
      260 262 267 269 270 276 278 281
    ],
    'Verizon' => %w[
      301 302 303 304 305 307 308 310 312 313
      314 315 316 317 318 319 320 321 323 325
      330 331 334 336 337 339 340 346 347 351
      352 360 361 364 380 385 386
    ],
    'T-Mobile' => %w[
      401 402 404 405 406 407 408 409 410 412
      413 414 415 417 419 423 424 425 430 432
      434 435 440 442 443 445 447 458 463 469
      470 475 478 479 480 484
    ]
  },
  ca: {
    'Rogers' => %w[416 647 437],
    'Bell' => %w[613 514 819],
    'Telus' => %w[604 778 250],
    'Freedom' => %w[343 365]
  },
  gb: {
    'EE' => %w[74],
    'Three' => %w[73],
    'Vodafone' => %w[77],
    'O2' => %w[75]
  },
  de: {
    'Telekom' => %w[151 160 170 171],
    'Vodafone' => %w[152 162 172 173],
    'O2' => %w[155 157 163 176 177 178 179]
  }
}.freeze
VERSION =
'0.7.0'
AREA_CODES =

Area code metadata for US, CA, GB, and DE

{
  us: {
    '212' => 'New York, NY',
    '213' => 'Los Angeles, CA',
    '214' => 'Dallas, TX',
    '215' => 'Philadelphia, PA',
    '216' => 'Cleveland, OH',
    '224' => 'Northern Illinois',
    '301' => 'Maryland',
    '302' => 'Delaware',
    '303' => 'Denver, CO',
    '305' => 'Miami, FL',
    '310' => 'Los Angeles, CA',
    '312' => 'Chicago, IL',
    '313' => 'Detroit, MI',
    '314' => 'St. Louis, MO',
    '323' => 'Los Angeles, CA',
    '347' => 'New York, NY',
    '404' => 'Atlanta, GA',
    '408' => 'San Jose, CA',
    '410' => 'Baltimore, MD',
    '412' => 'Pittsburgh, PA',
    '415' => 'San Francisco, CA',
    '425' => 'Bellevue, WA',
    '470' => 'Atlanta, GA',
    '480' => 'Phoenix, AZ',
    '503' => 'Portland, OR',
    '504' => 'New Orleans, LA',
    '510' => 'Oakland, CA',
    '512' => 'Austin, TX',
    '513' => 'Cincinnati, OH',
    '515' => 'Des Moines, IA',
    '516' => 'Long Island, NY',
    '551' => 'Northern New Jersey',
    '555' => 'Fictional/Reserved',
    '602' => 'Phoenix, AZ',
    '612' => 'Minneapolis, MN',
    '617' => 'Boston, MA',
    '619' => 'San Diego, CA',
    '626' => 'Pasadena, CA',
    '650' => 'San Mateo, CA',
    '702' => 'Las Vegas, NV',
    '703' => 'Northern Virginia',
    '704' => 'Charlotte, NC',
    '713' => 'Houston, TX',
    '714' => 'Orange County, CA',
    '718' => 'New York, NY',
    '720' => 'Denver, CO',
    '737' => 'Austin, TX',
    '747' => 'Los Angeles, CA',
    '773' => 'Chicago, IL',
    '801' => 'Salt Lake City, UT',
    '802' => 'Vermont',
    '808' => 'Hawaii',
    '818' => 'Los Angeles, CA',
    '832' => 'Houston, TX',
    '847' => 'Northern Illinois',
    '858' => 'San Diego, CA',
    '862' => 'Northern New Jersey',
    '901' => 'Memphis, TN',
    '917' => 'New York, NY',
    '919' => 'Raleigh, NC',
    '925' => 'East Bay, CA',
    '929' => 'New York, NY',
    '949' => 'Orange County, CA',
    '972' => 'Dallas, TX'
  },
  ca: {
    '204' => 'Manitoba',
    '226' => 'Southwestern Ontario',
    '236' => 'British Columbia',
    '249' => 'Northern Ontario',
    '250' => 'British Columbia',
    '289' => 'Greater Toronto Area',
    '306' => 'Saskatchewan',
    '343' => 'Eastern Ontario',
    '365' => 'Greater Toronto Area',
    '403' => 'Alberta (South)',
    '416' => 'Toronto, ON',
    '418' => 'Quebec City, QC',
    '431' => 'Manitoba',
    '437' => 'Toronto, ON',
    '438' => 'Montreal, QC',
    '450' => 'Greater Montreal',
    '506' => 'New Brunswick',
    '514' => 'Montreal, QC',
    '519' => 'Southwestern Ontario',
    '548' => 'Southwestern Ontario',
    '579' => 'Quebec',
    '581' => 'Quebec',
    '587' => 'Alberta',
    '604' => 'Vancouver, BC',
    '613' => 'Ottawa, ON',
    '639' => 'Saskatchewan',
    '647' => 'Toronto, ON',
    '672' => 'British Columbia',
    '705' => 'Northern Ontario',
    '709' => 'Newfoundland',
    '778' => 'British Columbia',
    '780' => 'Alberta (North)',
    '807' => 'Northwestern Ontario',
    '819' => 'Quebec (West)',
    '867' => 'Northern Territories',
    '873' => 'Quebec',
    '902' => 'Nova Scotia/PEI',
    '905' => 'Greater Toronto Area'
  },
  gb: {
    '20' => 'London',
    '21' => 'Birmingham',
    '23' => 'Southampton/Portsmouth',
    '24' => 'Coventry',
    '28' => 'Northern Ireland',
    '29' => 'Cardiff',
    '113' => 'Leeds',
    '114' => 'Sheffield',
    '115' => 'Nottingham',
    '116' => 'Leicester',
    '117' => 'Bristol',
    '118' => 'Reading',
    '121' => 'Birmingham',
    '131' => 'Edinburgh',
    '141' => 'Glasgow',
    '151' => 'Liverpool',
    '161' => 'Manchester',
    '171' => 'London (Historic)',
    '191' => 'Newcastle'
  },
  de: {
    '30' => 'Berlin',
    '40' => 'Hamburg',
    '69' => 'Frankfurt',
    '89' => 'Munich',
    '211' => 'Dusseldorf',
    '221' => 'Cologne',
    '228' => 'Bonn',
    '231' => 'Dortmund',
    '241' => 'Aachen',
    '251' => 'Munster',
    '341' => 'Leipzig',
    '351' => 'Dresden',
    '361' => 'Erfurt',
    '371' => 'Chemnitz',
    '381' => 'Rostock',
    '391' => 'Magdeburg',
    '511' => 'Hannover',
    '521' => 'Bielefeld',
    '551' => 'Gottingen',
    '611' => 'Wiesbaden',
    '621' => 'Mannheim',
    '711' => 'Stuttgart',
    '721' => 'Karlsruhe',
    '911' => 'Nuremberg'
  }
}.freeze
COUNTRIES =
{
  us: { code: '1', lengths: [10], format: '(%s) %s-%s', groups: [3, 3, 4], name: 'United States' },
  ca: { code: '1', lengths: [10], format: '(%s) %s-%s', groups: [3, 3, 4], name: 'Canada' },
  gb: { code: '44', lengths: [10, 11], format: '%s %s', groups: [4, 6], name: 'United Kingdom' },
  de: { code: '49', lengths: [10, 11], format: '%s %s', groups: [4, 7], name: 'Germany' },
  fr: { code: '33', lengths: [9], format: '%s %s %s %s %s', groups: [1, 2, 2, 2, 2], name: 'France' },
  au: { code: '61', lengths: [9], format: '%s %s %s', groups: [4, 3, 3], name: 'Australia' },
  jp: { code: '81', lengths: [10, 11], format: '%s-%s-%s', groups: [2, 4, 4], name: 'Japan' },
  in: { code: '91', lengths: [10], format: '%s %s', groups: [5, 5], name: 'India' },
  br: { code: '55', lengths: [10, 11], format: '(%s) %s-%s', groups: [2, 5, 4], name: 'Brazil' },
  mx: { code: '52', lengths: [10], format: '%s %s %s', groups: [3, 3, 4], name: 'Mexico' },
  es: { code: '34', lengths: [9], format: '%s %s %s', groups: [3, 3, 3], name: 'Spain' },
  it: { code: '39', lengths: [9, 10], format: '%s %s', groups: [3, 7], name: 'Italy' },
  nl: { code: '31', lengths: [9], format: '%s %s', groups: [3, 6], name: 'Netherlands' },
  be: { code: '32', lengths: [8, 9], format: '%s %s %s', groups: [3, 3, 3], name: 'Belgium' },
  ch: { code: '41', lengths: [9], format: '%s %s %s', groups: [3, 3, 3], name: 'Switzerland' },
  at: { code: '43', lengths: [10, 11], format: '%s %s', groups: [4, 7], name: 'Austria' },
  se: { code: '46', lengths: [9, 10], format: '%s-%s %s', groups: [3, 3, 4], name: 'Sweden' },
  no: { code: '47', lengths: [8], format: '%s %s %s', groups: [3, 2, 3], name: 'Norway' },
  dk: { code: '45', lengths: [8], format: '%s %s %s %s', groups: [2, 2, 2, 2], name: 'Denmark' },
  fi: { code: '358', lengths: [9, 10], format: '%s %s', groups: [3, 7], name: 'Finland' },
  pl: { code: '48', lengths: [9], format: '%s %s %s', groups: [3, 3, 3], name: 'Poland' },
  pt: { code: '351', lengths: [9], format: '%s %s %s', groups: [3, 3, 3], name: 'Portugal' },
  ie: { code: '353', lengths: [9, 10], format: '%s %s', groups: [3, 7], name: 'Ireland' },
  ru: { code: '7', lengths: [10], format: '(%s) %s-%s-%s', groups: [3, 3, 2, 2], name: 'Russia' },
  cn: { code: '86', lengths: [11], format: '%s %s %s', groups: [3, 4, 4], name: 'China' },
  kr: { code: '82', lengths: [10, 11], format: '%s-%s-%s', groups: [3, 4, 4], name: 'South Korea' },
  sg: { code: '65', lengths: [8], format: '%s %s', groups: [4, 4], name: 'Singapore' },
  nz: { code: '64', lengths: [8, 9, 10], format: '%s %s', groups: [3, 7], name: 'New Zealand' },
  za: { code: '27', lengths: [9], format: '%s %s %s', groups: [3, 3, 3], name: 'South Africa' },
  ng: { code: '234', lengths: [10, 11], format: '%s %s %s', groups: [3, 4, 4], name: 'Nigeria' },
  ke: { code: '254', lengths: [9, 10], format: '%s %s', groups: [3, 7], name: 'Kenya' },
  eg: { code: '20', lengths: [10], format: '%s %s %s', groups: [3, 4, 3], name: 'Egypt' },
  ar: { code: '54', lengths: [10], format: '%s %s-%s', groups: [3, 3, 4], name: 'Argentina' },
  cl: { code: '56', lengths: [9], format: '%s %s %s', groups: [3, 3, 3], name: 'Chile' },
  co: { code: '57', lengths: [10], format: '%s %s %s', groups: [3, 3, 4], name: 'Colombia' },
  pe: { code: '51', lengths: [9], format: '%s %s %s', groups: [3, 3, 3], name: 'Peru' }
}.freeze
COUNTRY_CODE_MAP =
COUNTRIES.each_with_object({}) do |(sym, data), map|
  map[data[:code]] ||= []
  map[data[:code]] << sym
end.freeze
SHORTCODE_RULES =

SMS shortcode validation rules per country

{
  us: { lengths: [5, 6] },
  ca: { lengths: [5, 6] },
  gb: { lengths: [5, 6] },
  de: { lengths: [4, 5] },
  fr: { lengths: [5] },
  au: { lengths: [6] },
  in: { lengths: [5, 6] },
  br: { lengths: [5] },
  mx: { lengths: [5] },
  jp: { lengths: [4, 5] },
  kr: { lengths: [4] },
  it: { lengths: [5] },
  es: { lengths: [5, 6] }
}.freeze
PHONE_TYPE_PATTERNS =

Phone type detection based on prefix patterns per country

{
  us: {
    toll_free: /\A(800|888|877|866|855|844|833)\d{7}\z/,
    premium: /\A(900|976)\d{7}\z/,
    mobile: /\A[2-9]\d{2}[2-9]\d{6}\z/
  },
  ca: {
    toll_free: /\A(800|888|877|866|855|844|833)\d{7}\z/,
    premium: /\A(900|976)\d{7}\z/,
    mobile: /\A[2-9]\d{2}[2-9]\d{6}\z/
  },
  gb: {
    toll_free: /\A(800|808)\d{6,7}\z/,
    premium: /\A(90[0-9]|91[0-9])\d{7}\z/,
    mobile: /\A7[1-9]\d{8}\z/,
    landline: /\A(1|2)\d{8,9}\z/
  },
  de: {
    toll_free: /\A800\d{7}\z/,
    premium: /\A(900|906)\d{6,7}\z/,
    mobile: /\A(15|16|17)\d{8,9}\z/,
    landline: /\A(2|3|4|5|6|7|8|9)\d{8,9}\z/
  },
  fr: {
    toll_free: /\A800\d{6}\z/,
    premium: /\A8[1-9]\d{7}\z/,
    mobile: /\A[67]\d{8}\z/,
    landline: /\A[1-5]\d{8}\z/
  },
  au: {
    toll_free: /\A(1800)\d{6}\z/,
    premium: /\A(190[0-9])\d{6}\z/,
    mobile: /\A4\d{8}\z/,
    landline: /\A[2378]\d{8}\z/
  },
  jp: {
    toll_free: /\A(120|800)\d{7}\z/,
    mobile: /\A[789]0\d{8}\z/,
    landline: /\A[1-6]\d{8,9}\z/
  },
  in: {
    toll_free: /\A(1800)\d{6,7}\z/,
    mobile: /\A[6-9]\d{9}\z/,
    landline: /\A[1-5]\d{9}\z/
  },
  br: {
    toll_free: /\A(0800)\d{7}\z/,
    mobile: /\A\d{2}9\d{8}\z/,
    landline: /\A\d{2}[2-5]\d{7}\z/
  },
  mx: {
    mobile: /\A[1-9]\d{9}\z/
  },
  es: {
    toll_free: /\A(800|900)\d{6}\z/,
    mobile: /\A[67]\d{8}\z/,
    landline: /\A[89]\d{8}\z/
  },
  it: {
    toll_free: /\A800\d{6}\z/,
    mobile: /\A3\d{8,9}\z/,
    landline: /\A0\d{8,9}\z/
  },
  ru: {
    toll_free: /\A(800)\d{7}\z/,
    mobile: /\A9\d{9}\z/,
    landline: /\A[3-8]\d{9}\z/
  },
  cn: {
    mobile: /\A1[3-9]\d{9}\z/,
    landline: /\A[2-9]\d{9,10}\z/
  },
  kr: {
    toll_free: /\A(80)\d{7,8}\z/,
    mobile: /\A(10|11|16|17|18|19)\d{7,8}\z/,
    landline: /\A(2|3[1-3]|4[1-4]|5[1-5]|6[1-4])\d{7,8}\z/
  },
  nl: {
    toll_free: /\A(800|900)\d{4,7}\z/,
    premium: /\A(906|909)\d{4,7}\z/,
    mobile: /\A6\d{8}\z/,
    landline: /\A[1-5]\d{7,8}\z/
  },
  be: {
    toll_free: /\A(800)\d{5}\z/,
    premium: /\A(900|70)\d{5,6}\z/,
    mobile: /\A4\d{7,8}\z/,
    landline: /\A[1-9]\d{6,7}\z/
  },
  ch: {
    toll_free: /\A(800)\d{6}\z/,
    premium: /\A(900|901)\d{6}\z/,
    mobile: /\A7[5-9]\d{7}\z/,
    landline: /\A[2-6]\d{7,8}\z/
  },
  at: {
    toll_free: /\A(800)\d{6,7}\z/,
    premium: /\A(900|901)\d{6,7}\z/,
    mobile: /\A(650|660|664|676|680|681|688|699)\d{6,7}\z/,
    landline: /\A[1-5]\d{8,9}\z/
  },
  se: {
    toll_free: /\A(20)\d{6,7}\z/,
    premium: /\A(900|939)\d{5,6}\z/,
    mobile: /\A7\d{8}\z/,
    landline: /\A[1-6]\d{7,8}\z/
  },
  no: {
    toll_free: /\A(800)\d{5}\z/,
    premium: /\A(820|829)\d{5}\z/,
    mobile: /\A[49]\d{7}\z/,
    landline: /\A[2-3]\d{7}\z/
  },
  dk: {
    toll_free: /\A(80)\d{6}\z/,
    premium: /\A(90)\d{6}\z/,
    mobile: /\A[2-4]\d{7}\z/,
    landline: /\A[3-9]\d{7}\z/
  },
  fi: {
    toll_free: /\A(800)\d{6,7}\z/,
    premium: /\A(600)\d{6,7}\z/,
    mobile: /\A(4[0-9]|50)\d{6,7}\z/,
    landline: /\A[1-3]\d{7,8}\z/
  },
  pl: {
    toll_free: /\A(800)\d{6}\z/,
    premium: /\A(70)\d{7}\z/,
    mobile: /\A[5-7]\d{8}\z/,
    landline: /\A[1-4]\d{7,8}\z/
  },
  pt: {
    toll_free: /\A(800)\d{6}\z/,
    premium: /\A(760)\d{6}\z/,
    mobile: /\A9[1-3,6]\d{7}\z/,
    landline: /\A2\d{8}\z/
  },
  ie: {
    toll_free: /\A(1800)\d{5,6}\z/,
    premium: /\A(15[12])\d{6,7}\z/,
    mobile: /\A8[35-9]\d{7}\z/,
    landline: /\A[1-6]\d{7,8}\z/
  },
  sg: {
    toll_free: /\A(1800)\d{4}\z/,
    mobile: /\A[89]\d{7}\z/,
    landline: /\A6\d{7}\z/
  },
  nz: {
    toll_free: /\A(800)\d{5,7}\z/,
    premium: /\A(900)\d{5,7}\z/,
    mobile: /\A2[0-9]\d{6,7}\z/,
    landline: /\A[3-9]\d{6,7}\z/
  },
  za: {
    toll_free: /\A(800)\d{6}\z/,
    premium: /\A(86[01])\d{6}\z/,
    mobile: /\A[67]\d{8}\z/,
    landline: /\A[1-5]\d{8}\z/
  },
  ng: {
    toll_free: /\A(800)\d{7,8}\z/,
    mobile: /\A[789]0\d{7,8}\z/,
    landline: /\A[1-9]\d{8,9}\z/
  },
  ke: {
    toll_free: /\A(800)\d{6}\z/,
    mobile: /\A7\d{8}\z/,
    landline: /\A[2-6]\d{7,8}\z/
  },
  eg: {
    toll_free: /\A(800)\d{7}\z/,
    mobile: /\A1[0-2]\d{8}\z/,
    landline: /\A[2-5]\d{8}\z/
  },
  ar: {
    toll_free: /\A(800)\d{7}\z/,
    mobile: /\A9\d{9}\z/,
    landline: /\A[1-8]\d{8,9}\z/
  },
  cl: {
    toll_free: /\A(800)\d{6}\z/,
    mobile: /\A9\d{8}\z/,
    landline: /\A[2-7]\d{7,8}\z/
  },
  co: {
    toll_free: /\A(800)\d{7}\z/,
    mobile: /\A3\d{9}\z/,
    landline: /\A[1-8]\d{8,9}\z/
  },
  pe: {
    toll_free: /\A(800)\d{5,6}\z/,
    mobile: /\A9\d{8}\z/,
    landline: /\A[1-8]\d{7,8}\z/
  }
}.freeze

Class Method Summary collapse

Class Method Details

.parse(input, country: nil) ⇒ Object



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/philiprehberger/phone.rb', line 132

def self.parse(input, country: nil)
  cleaned = input.to_s.strip
  return PhoneNumber.new(country_code: '', national: '', country: nil) if cleaned.empty?

  has_plus = cleaned.start_with?('+')
  digits = cleaned.gsub(/[^\d]/, '')

  if has_plus
    cc, national, sym = detect_country_from_digits(digits)
  elsif country
    data = COUNTRIES[country]
    raise ParseError, "unknown country: #{country}" unless data

    cc = data[:code]
    national = digits.delete_prefix(cc)
    sym = country
  else
    cc, national, sym = detect_country_from_digits(digits)
  end

  PhoneNumber.new(country_code: cc, national: national, country: sym)
end

.valid?(input, country: nil) ⇒ Boolean

Returns:

  • (Boolean)


155
156
157
158
159
# File 'lib/philiprehberger/phone.rb', line 155

def self.valid?(input, country: nil)
  parse(input, country: country).valid?
rescue ParseError
  false
end

.valid_shortcode?(input, country: :us) ⇒ Boolean

Returns:

  • (Boolean)


165
166
167
# File 'lib/philiprehberger/phone.rb', line 165

def self.valid_shortcode?(input, country: :us)
  ShortcodeValidation.valid_shortcode?(input, country: country)
end

.vanity_to_digits(input) ⇒ Object



161
162
163
# File 'lib/philiprehberger/phone.rb', line 161

def self.vanity_to_digits(input)
  VanityConversion.vanity_to_digits(input)
end