# An object representing a phone number. # # The phone number is recorded in 3 separate parts: # * country_code - e.g. '385', '386' # * area_code - e.g. '91', '47' # * number - e.g. '5125486', '451588' # # All parts are mandatory, but country code and area code can be set for all phone numbers using # Phone.default_country_code # Phone.default_area_code # require 'active_support' require File.join(File.dirname(__FILE__), 'phone_country') class Phone NUMBER = '([^0][0-9]{1,7})$' DEFAULT_AREA_CODE = '[2-9][0-8][0-9]' # USA attr_accessor :country_code, :area_code, :number cattr_accessor :default_country_code cattr_accessor :default_area_code cattr_accessor :named_formats # length of first number part (using multi number format) cattr_accessor :n1_length # default length of first number part @@n1_length = 3 @@named_formats = { :default => "+%c%a%n", :europe => '+%c (0) %a %f %l', :us => "(%a) %f-%l" } def initialize(*hash_or_args) if hash_or_args.first.is_a?(Hash) hash_or_args = hash_or_args.first keys = {:number => :number, :area_code => :area_code, :country_code => :country_code} else keys = {:number => 0, :area_code => 1, :country_code => 2} end self.number = hash_or_args[ keys[:number] ] self.area_code = hash_or_args[ keys[:area_code] ] || self.default_area_code self.country_code = hash_or_args[ keys[:country_code] ] || self.default_country_code raise "Must enter number" if self.number.blank? raise "Must enter area code or set default area code" if self.area_code.blank? raise "Must enter country code or set default country code" if self.country_code.blank? end # create a new phone number by parsing a string # the format of the string is detect automatically (from FORMATS) def self.parse(string, options={}) if string.present? PhoneCountry.load string = normalize(string) options[:country_code] ||= self.default_country_code options[:area_code] ||= self.default_area_code parts = split_to_parts(string, options) pn = Phone.new(parts) if parts end end # is this string a valid phone number? def self.valid?(string) begin parse(string).present? rescue RuntimeError # if we encountered exceptions (missing country code, missing area code etc) return false end end # split string into hash with keys :country_code, :area_code and :number def self.split_to_parts(string, options = {}) country = detect_country(string) if country options[:country_code] = country.country_code string = string.gsub(country.country_code_regexp, '0') else if options[:country_code] country = PhoneCountry.find_by_country_code options[:country_code] end end if country.nil? if options[:country_code].nil? raise "Must enter country code or set default country code" else raise "Could not find country with country code #{options[:country_code]}" end end format = detect_format(string, country) return nil if format.nil? parts = string.match formats(country)[format] case format when :short {:number => parts[2], :area_code => parts[1], :country_code => options[:country_code]} when :really_short {:number => parts[1], :area_code => options[:area_code], :country_code => options[:country_code]} end end # detect country from the string entered def self.detect_country(string) detected_country = nil # find if the number has a country code PhoneCountry.all.each_pair do |country_code, country| if string =~ country.country_code_regexp detected_country = country end end detected_country end def self.formats(country) area_code_regexp = country.area_code || DEFAULT_AREA_CODE { # 047451588, 013668734 :short => Regexp.new('^0(' + area_code_regexp + ')' + NUMBER), # 451588 :really_short => Regexp.new('^' + NUMBER) } end # detect format (from FORMATS) of input string def self.detect_format(string_with_number, country) arr = [] formats(country).each_pair do |format, regexp| arr << format if string_with_number =~ regexp end raise "Detected more than 1 format for #{string_with_number}" if arr.size > 1 arr.first end # fix string so it's easier to parse, remove extra characters etc. def self.normalize(string_with_number) string_with_number.gsub("(0)", "").gsub(/[^0-9+]/, '').gsub(/^00/, '+') end # format area_code with trailing zero (e.g. 91 as 091) def area_code_long "0" + area_code if area_code end # first n characters of :number def number1 number[0...self.class.n1_length] end # everything left from number after the first n characters (see number1) def number2 n2_length = number.size - self.class.n1_length number[-n2_length, n2_length] end # Formats the phone number. # # if the method argument is a String, it is used as a format string, with the following fields being interpolated: # # * %c - country_code (385) # * %a - area_code (91) # * %A - area_code with leading zero (091) # * %n - number (5125486) # * %n1 - first @@n1_length characters of number (configured through Phone.n1_length), default is 3 (512) # * %n2 - last characters of number (5486) # # if the method argument is a Symbol, it is used as a lookup key for a format String in Phone.named_formats # pn.format(:europe) def format(fmt) if fmt.is_a?(Symbol) raise "The format #{fmt} doesn't exist'" unless named_formats.has_key?(fmt) format_number named_formats[fmt] else format_number(fmt) end end # the default format is "+%c%a%n" def to_s format(:default) end # does this number belong to the default country code? def has_default_country_code? country_code == self.class.default_country_code end # does this number belong to the default area code? def has_default_area_code? area_code == self.class.default_area_code end private def format_number(fmt) fmt.gsub("%c", country_code || ""). gsub("%a", area_code || ""). gsub("%A", area_code_long || ""). gsub("%n", number || ""). gsub("%f", number1 || ""). gsub("%l", number2 || "") end end