Created
May 23, 2025 23:10
-
-
Save bensheldon/e29fbd33f222eba0b48fa62544797277 to your computer and use it in GitHub Desktop.
A Phone Number tiny type
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
class PhoneNumber | |
EXACT_DIGITS = 10 | |
MAX_NON_DIGITS = 5 | |
delegate :present?, :blank?, :encoding, to: :@value | |
delegate :hash, to: :strict | |
def initialize(value) | |
@value = value | |
end | |
# Format: +1112223333 or unaltered string | |
def strict | |
normalize(@value) | |
end | |
# Strict format or raise ArgumentError | |
def strict! | |
valid? ? strict : raise(ArgumentError, "Invalid strict phone number: #{@value}") | |
end | |
# In strict format already | |
def valid? | |
normalize(@value)&.match?(/\A\+\d{11}\z/) | |
end | |
def loose_valid? | |
return false if @value.blank? | |
digits = @value.to_s.gsub(/\D/, '') | |
non_digits = @value.to_s.gsub(/\d/, '') | |
digits.size == EXACT_DIGITS && non_digits.size <= MAX_NON_DIGITS | |
end | |
# 555-555-5555 | |
def display | |
return @value unless valid? | |
strict.to_s.delete_prefix('+1').insert(6, '-').insert(3, '-') | |
end | |
def ==(other) | |
strict == if other.is_a?(PhoneNumber) | |
other.strict | |
else | |
normalize(other) | |
end | |
end | |
def presence | |
present? ? self : nil | |
end | |
def to_s | |
@value | |
end | |
# Allow comparison of "string" == PhoneNumber | |
alias to_str to_s | |
private | |
# Turn into +1112223333, nil if blank, or unmodified value | |
def normalize(string) | |
return nil if string.blank? | |
digits = string.to_s.strip.gsub(/\D/, '') | |
if digits.size == 10 | |
"+1#{digits}" | |
elsif digits.size == 11 && digits.start_with?('1') | |
"+#{digits}" | |
else | |
string | |
end | |
end | |
class Attribute < ActiveRecord::Type::Text | |
def cast(value) | |
return value if value.is_a?(PhoneNumber) | |
return nil if value.nil? | |
PhoneNumber.new(value) | |
end | |
def serialize(value) | |
if value.present? | |
value.is_a?(PhoneNumber) ? value.strict! : PhoneNumber.new(value).strict! | |
end | |
end | |
def deserialize(value) | |
value.nil? ? nil : PhoneNumber.new(value) | |
end | |
def changed_in_place?(raw_old_value, new_value) | |
raw_old_value != serialize(new_value) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment