├── init.rb ├── README ├── bin └── zippy └── lib └── zippy.rb /init.rb: -------------------------------------------------------------------------------- 1 | require 'zippy' 2 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Zippy! 2 | ------ 3 | 4 | http://github.com/defunkt/zippy 5 | 6 | By Chris "Zippy" Wanstrath 7 | -------------------------------------------------------------------------------- /bin/zippy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'zippy' 4 | 5 | puts Zippy.find(ARGV[0], ARGV[1]).inspect 6 | -------------------------------------------------------------------------------- /lib/zippy.rb: -------------------------------------------------------------------------------- 1 | %w( rubygems cgi hpricot net/http open-uri ).each { |f| require f } 2 | # => chris[at]ozmm[dot]org 3 | 4 | ## 5 | # Finds all zip codes for a single city and state. 6 | # 7 | # >> Zippy.find('Cincinnati', 'OH') 8 | # => ["45201", "45202", "45203", "45204", ... ] 9 | # 10 | class Zippy 11 | Host = 'zip4.usps.com' 12 | Page = '/zip4/zcl_1_results.jsp' 13 | 14 | attr_accessor :zips, :city, :state 15 | 16 | def self.find(city, state) 17 | new(city, state).zips 18 | end 19 | 20 | def initialize(city, state) 21 | @city, @state = city, state 22 | find 23 | end 24 | 25 | def find 26 | doc = Hpricot(post_body || '') 27 | cells = Array(doc.search(:td)) 28 | 29 | @zips = cells.map do |td| 30 | next unless td['headers'] == 'units' 31 | td.innerText[/(\d){5}/] if td.respond_to?(:innerText) 32 | end 33 | 34 | @zips.compact! 35 | end 36 | 37 | private 38 | def post_body 39 | Net::HTTP.new(Host, 80).post(Page, post_params, headers).body rescue nil 40 | end 41 | 42 | def post_params 43 | params = "city=#{CGI.escape(@city.to_s.upcase)}&state=#{CGI.escape(@state.to_s.upcase)}" 44 | params << "&visited=1&pagenumber=all" 45 | end 46 | 47 | def headers 48 | { 'Content-Type' => 'application/x-www-form-urlencoded' } 49 | end 50 | end 51 | 52 | if $0 == __FILE__ 53 | %w( test/spec mocha zlib base64 ).each { |f| require f } 54 | 55 | html = Zlib::Inflate.new.inflate(Base64.decode64(DATA.read)) 56 | 57 | context "Searching on a valid city and state" do 58 | setup do 59 | Zippy.any_instance.stubs(:post_body).returns(html) 60 | end 61 | 62 | specify "should return an array of zipcodes" do 63 | zips = Zippy.find('Cincinnati', 'OH') 64 | zips.should.be.an.instance_of Array 65 | zips.size.should.be > 0 66 | zips.first.should.match /(\d){5}/ 67 | end 68 | end 69 | 70 | context "Searching on an invalid city and state" do 71 | setup do 72 | Zippy.any_instance.stubs(:post_body).returns(nil) 73 | end 74 | 75 | specify "should return an empty array" do 76 | zips = Zippy.find('Dogtown', 'OH') 77 | zips.should.be.an.instance_of Array 78 | zips.should.be.empty 79 | end 80 | end 81 | end 82 | 83 | __END__ 84 | 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 141 | Q0hSFtwTH9rewR78e6AwcaCVgbyLiU+2dWbn8vME4j0EoEwp4AeUJEiHSdTV 142 | 9VVbGDwMoqArPVuqyzOnyMhp71UT901FrMSfl7Vfv79prEjaIOEQMmVgTAne 143 | 4SvATvx3NbfSGn0ngnF+6Qli1kBqlEwnCaDiFK1SPE8k75W8BUDWCyDzAbJR 144 | AJkPkM0CyHyAbBVA5gNkuwAyHyAPCyDzAbJTAJkLkJpWAJkPkEVCnhOQRUKe 145 | E5BFQp4TkEVCnhOQRUKeE5BFQp4TkEVCnhOQRUKeD5B6bduBRM/g8ZP+0OOn 146 | j2en//54Ujx+yunxk17brudPem3dA6jIgzdOrwdj1DNglv4As4oHu/kya8ue 147 | 7OrFo92vzQCKAkhOQBYFkJyALAogOQFZFEByArIogOQEZFEAyQnIogCSD5D1 148 | rS+APBcgi0e7OQFZPNrNCcjt39nsfFcMBYiN+KwowGygAFOvb1cBpl7f3gLM 149 | M2BXs2DXRtnV2DJ2NbaXXc8kCShqIDkBWdRAcgKyqIHkBGRRA8kHyEZRA8kJ 150 | yKIGkhOQRQ0kJyC3vwbyTIAsnu7mBGTxdDcnIIudTU5AFjubnIAsdjY5Abn9 151 | O5ud74ohB7FZi8+K2uoGaquNznbVVhudorb6dWGqWWx3cwJy+7e7CTu+W7yv 152 | F/F+k/G+qW9XvG/q2xvvnwG7ivdANsuuLXsPpLnF74E8kyRg+wtjzyBMHRZh 153 | aqNhqrllYaq5vWHq+7OrVfzfuJtlV2vL2NXaWnY9h6+RaD20gy2+RiJferW3 154 | jF7trfwaiWdBrYe2rwW18qXW4ZZR67Cg1lOp1S6otVFqbdlztuba52wFtdZR 155 | 66FqRkGtXKnV2rLvFWtt5/eKPQtqdQpqbZRaW/bFYq21XyxWUGsNtdoPvXhV 156 | UCtfam3Zg/jW2gfxBbXWUav4otnNUmvLnsK31j6FL6i1jlpF8X2z1Nqyr2Jo 157 | rf0qhoJa66hVFN83S60teymjtfaljGdLrefArOL7gzbLrC17IaNVvJDxVfQq 158 | Hm1tll5b9kJGaztfyHgGzDos/ufMzTJry97HaK19H6NYuB6gV6dVLFwbpdeW 159 | vZPRKt7JeDK1incyNkqt9pa9k9Eu3sl4MrWKdzI2S60teyejXbyT8URqdQpq 160 | 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 197 | E6KVM/gflyMWTg== 198 | --------------------------------------------------------------------------------