Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit d8a8967bfe8bfef1e53041bab1135ce67235a4c7 @matsadler matsadler committed Nov 21, 2011
Showing with 633 additions and 0 deletions.
  1. +77 −0 README.rdoc
  2. +13 −0 going_postal.gemspec
  3. +220 −0 lib/going_postal.rb
  4. +21 −0 test/cover.rb
  5. +302 −0 test/going_postal_test.rb
@@ -0,0 +1,77 @@
+= GoingPostal
+
+The GoingPostal mixin provides classes with postcode formatting and validation
+methods.
+
+== Installation
+
+ $ gem install going_postal
+
+== Usage
+
+GoingPostal can be used as a mixin:
+
+ class Address
+ include GoingPostal
+ attr_accessor :number, :street, :city, :postcode, :country_code
+
+ def initialize(number, street, :city, postcode, country_code="GB")
+ self.number = number
+ self.street = street
+ self.city = city
+ self.postcode = postcode
+ self.country_code = country_code
+ end
+
+ def postcode=(value)
+ @postcode = format_postcode(value)
+ end
+
+ def valid?
+ number && street && city && postcode_valid?
+ end
+ end
+
+or as a namespaced collection of methods:
+
+ GoingPostal.postcode?("sl41eg", "GB") #=> "SL4 1EG"
+ GoingPostal.postcode?("200378001", "US") #=> "20037-8001"
+
+The methods available are #postcode? for checking validity, and format_postcode
+which returns a formatted postcode. Both take arguments of a string, the
+postcode, and a two letter country code. If the class has a #country_code
+method, the country_code argument on the provided methods becomes optional. If
+the class also has one of #postcode, #post_code, #zipcode, #zip_code, or #zip,
+the string argument on the provided methods becomes optional.
+
+postcode? is aliased as post_code?, zip?, zipcode?, zip_code?, valid_postcode?,
+valid_post_code?, valid_zip?, valid_zipcode?, valid_zip_code?, postcode_valid?,
+post_code_valid?, zip_valid?, zipcode_valid? and zip_code_valid? The alias
+valid? is also available directly on the GoingPostal module.
+
+format_postcode is aliased as format_post_code, format_zip, format_zipcode, and
+format_zip_code.
+
+== Licence
+
+(The MIT License)
+
+Copyright (c) 2011 Global Personals, Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
@@ -0,0 +1,13 @@
+Gem::Specification.new do |s|
+ s.name = "going_postal"
+ s.version = "0.1.0"
+ s.summary = "Global post/zip code formatting and validation mixin"
+ s.description = "Post/zip code formatting and validation for the UK, US, CA and more."
+ s.files = %W{lib}.map {|dir| Dir["#{dir}/**/*.rb"]}.flatten << "README.rdoc"
+ s.require_path = "lib"
+ s.rdoc_options = ["--main", "README.rdoc", "--charset", "utf-8"]
+ s.extra_rdoc_files = ["README.rdoc"]
+ s.author = "Matthew Sadler"
+ s.email = "mat@sourcetagsandcodes.com"
+ s.homepage = "http://github.com/globaldev/going_postal"
+end
@@ -0,0 +1,220 @@
+# The GoingPostal mixin provides classes with postcode formatting and validation
+# methods. If the class has a #country_code method, the country_code argument on
+# the provided methods becomes optional. If the class also has one of #postcode,
+# #post_code, #zipcode, #zip_code, or #zip, the string argument on the provided
+# methods becomes optional.
+#
+# class Address
+# include GoingPostal
+# attr_accessor :number, :street, :city, :postcode, :country_code
+#
+# def initialize(number, street, :city, postcode, country_code="GB")
+# self.number = number
+# self.street = street
+# self.city = city
+# self.postcode = postcode
+# self.country_code = country_code
+# end
+#
+# def postcode=(value)
+# @postcode = format_postcode(value)
+# end
+#
+# def valid?
+# number && street && city && postcode_valid?
+# end
+# end
+#
+# The methods can also be called directly on the GoingPostal module.
+#
+# GoingPostal.postcode?("sl41eg", "GB") #=> "SL4 1EG"
+# GoingPostal.postcode?("200378001", "US") #=> "20037-8001"
+#
+# Currently supported countries are United Kingdom (GB, UK), United States (US),
+# Canada (CA), Australia (AU), New Zeland (NZ), and South Africa (ZA).
+#
+# Ireland (IE) is supported insomuch as, Ireland doesn't use postcodes, so "" or
+# nil are considered valid.
+#
+# Currently unsupported countries will be formatted by simply stripping leading
+# and trailing whitespace, and any input will be considered valid.
+#
+module GoingPostal
+ extend self
+
+ # :section: Validation
+
+ # :call-seq: postcode?([string[, country_code]]) -> formatted_str or false
+ #
+ # Returns a formatted postcode as a string if string is a valid postcode,
+ # false otherwise.
+ #
+ # If calling this method on the GoingPostal module the country_code argument
+ # is required and should be a two letter country code.
+ #
+ # If the GoingPostal module has been mixed in to a class, and the class has
+ # a #country_code method, the country_code argument is optional, defaulting
+ # to the value returned by #country_code. If the class also has a #postcode,
+ # #post_code, #zipcode, #zip_code, or #zip method, the string argument becomes
+ # optional.
+ #
+ # Postcodes for unknown countries will always be considered valid, the return
+ # value will consist of the input stripped of leading and trailing whitespace.
+ #
+ # Ireland (IE) has no postcodes, "" will be returned from in input of "" or
+ # nil, false otherwise.
+ #
+ def postcode?(*args)
+ string, country_code = get_args_for_format_postcode(args)
+ case country_code.to_s.upcase
+ when "GB", "UK", "US", "USA", "CA", "AU", "NZ", "ZA"
+ format_postcode(string, country_code) || false
+ when "IE"
+ string.nil? || string.to_s.empty? ? "" : false
+ else
+ format_postcode(string, country_code)
+ end
+ end
+ alias post_code? postcode?
+ alias zip? postcode?
+ alias zipcode? postcode?
+ alias zip_code? postcode?
+ alias valid_postcode? postcode?
+ alias valid_post_code? postcode?
+ alias valid_zip? postcode?
+ alias valid_zipcode? postcode?
+ alias valid_zip_code? postcode?
+ alias postcode_valid? postcode?
+ alias post_code_valid? postcode?
+ alias zip_valid? postcode?
+ alias zipcode_valid? postcode?
+ alias zip_code_valid? postcode?
+
+ # :call-seq: self.valid?([string[, country_code]]) -> formatted_string or false
+ #
+ # Alias for #postcode?
+ #--
+ # this is just here to trick rdoc, the class alias below will overwrite this
+ # empty method.
+ #++
+ def self.valid?; end
+
+ class << self
+ alias valid? postcode?
+ end
+
+ # :section: Formatting
+
+ # :call-seq: format_postcode([string[, country_code]]) -> formatted_str or nil
+ #
+ # Returns a formatted postcode as a string if string is a valid postcode, nil
+ # otherwise.
+ #
+ # If calling this method on the GoingPostal module the country_code argument
+ # is required and should be a two letter country code.
+ #
+ # If the GoingPostal module has been mixed in to a class, and the class has
+ # a #country_code method, the country_code argument is optional, defaulting
+ # to the value returned by #country_code. If the class also has a #postcode,
+ # #post_code, #zipcode, #zip_code, or #zip method, the string argument becomes
+ # optional.
+ #
+ # Postcodes for unknown countries will simply be stripped of leading and
+ # trailing whitespace.
+ #
+ # Ireland (IE) has no postcodes, so nil will always be returned.
+ #
+ def format_postcode(*args)
+ string, country_code = get_args_for_format_postcode(args)
+ case country_code.to_s.upcase
+ when "GB", "UK"
+ format_gb_postcode(string)
+ when "US", "USA"
+ format_us_zipcode(string)
+ when "CA"
+ format_ca_postcode(string)
+ when "AU", "NZ", "ZA"
+ format_au_postcode(string)
+ when "IE"
+ nil
+ else
+ string.to_s.strip
+ end
+ end
+ alias format_post_code format_postcode
+ alias format_zip format_postcode
+ alias format_zipcode format_postcode
+ alias format_zip_code format_postcode
+
+ # :stopdoc:
+
+ def format_gb_postcode(string)
+ out_code = string.to_s.upcase.delete(" \t\r\n")
+ in_code = out_code.slice!(-3, 3)
+ if out_code =~ /^[A-Z]{1,2}([1-9][0-9A-HJKMNPR-Y]?|0[A-HJKMNPR-Y]?)$/ &&
+ in_code =~ /^[0-9][A-HJLNP-Z]{2}$/
+ [out_code, in_code].join(" ")
+ end
+ end
+ alias format_uk_postcode format_gb_postcode
+
+ def format_ca_postcode(string)
+ forward_sort_area = string.to_s.upcase.delete(" \t\r\n")
+ local_delivery_unit = forward_sort_area.slice!(-3, 3)
+ if forward_sort_area =~ /^[A-CEGHJK-NPR-TVXY][0-9][A-CEGHJK-NPR-TV-Z]$/ &&
+ local_delivery_unit =~ /[0-9][A-CEGHJK-NPR-TV-Z][0-9]/
+ [forward_sort_area, local_delivery_unit].join(" ")
+ end
+ end
+
+ def format_au_postcode(string)
+ string = string.to_s.delete(" \t\r\n")
+ string if string =~ /^[0-9]{4}$/
+ end
+ alias format_nz_postcode format_au_postcode
+ alias format_za_postcode format_au_postcode
+
+ def format_us_zipcode(string)
+ zip = string.to_s.delete("- \t\r\n")
+ plus_four = zip.slice!(5, 4)
+ plus_four = nil if plus_four && plus_four.empty?
+ if zip =~ /^[0-9]{5}$/ && (plus_four.nil? || plus_four =~ /^[0-9]{4}$/)
+ [zip, plus_four].compact.join("-")
+ end
+ end
+ alias format_us_postcode format_us_zipcode
+
+ private
+
+ def get_args_for_format_postcode(args)
+ case args.length
+ when 2
+ args
+ when 0
+ [postcode_for_format_postcode, country_code_for_format_postcode]
+ when 1
+ args << country_code_for_format_postcode
+ else
+ message = "wrong number of arguments (#{args.length} for 0..2)"
+ raise ArgumentError, message, caller(2)
+ end
+ end
+
+ def country_code_for_format_postcode
+ if respond_to?(:country_code)
+ country_code
+ else
+ raise ArgumentError, "wrong number of arguments (1 for 0..2)", caller(3)
+ end
+ end
+
+ POSTCODE_ALIASES = [:postcode, :post_code, :zipcode, :zip_code, :zip]
+ def postcode_for_format_postcode
+ if ali = POSTCODE_ALIASES.find {|a| respond_to?(a)}
+ send(ali)
+ else
+ raise ArgumentError, "wrong number of arguments (0 for 0..2)", caller(3)
+ end
+ end
+
+end
@@ -0,0 +1,21 @@
+require "coverage" # >= ruby 1.9 only
+
+testing = Dir[File.expand_path("../../lib/**/*.rb", __FILE__)]
+
+at_exit do
+ results = Coverage.result.select {|key, value| testing.include?(key)}
+
+ all = results.map(&:last).flatten.compact
+ print "\n#{all.reject(&:zero?).size}/#{all.size} executable lines covered\n\n"
+
+ results.each do |key, value|
+ next unless value.include?(0)
+ lines = File.readlines(key).zip(value).each_with_index.map do |(line,val),i|
+ "%-2s%3i %5s %s" % [(">" if val == 0), (i + 1), val, line]
+ end
+ print "#{key}\n line calls code\n\n#{lines.join}\n\n"
+ end
+end
+
+Coverage.start
+require_relative "going_postal_test"
Oops, something went wrong.

0 comments on commit d8a8967

Please sign in to comment.