# Justin Ethier
# September 16, 2007
#
# Ruby Quiz 139 - IP Address to Country Code
# For more information see http://www.rubyquiz.com/quiz139.htm
#
Filename = "IpToCountry.csv" # IP to Country Code database file
# Utility method to convert an IP (V4) address (as string) to a decimal number
# Example from file:
# 1.2.3.4 = 4 + (3 * 256) + (2 * 256 * 256) + (1 * 256 * 256 * 256)
# is 4 + 768 + 13,1072 + 16,777,216 = 16,909,060
def convert_ip_str_to_num(ip_addr_str)
ip_addr = ip_addr_str.split(".")
ip_addr_num = ip_addr[0].to_i * 256 ** 3 +
ip_addr[1].to_i * 256 ** 2 +
ip_addr[2].to_i * 256 ** 1 +
ip_addr[3].to_i
return ip_addr_num
end
# Utility method to parse an IP range field of the IP/Country DB file
def parse_line(line)
line_parsed = line.split(',')
# Validate to handle comments and unexpected data
if line_parsed != nil and line_parsed.size >= 5
from = line_parsed[0].tr('"', '').to_i
to = line_parsed[1].tr('"', '').to_i
country_code = line_parsed[4]
end
return from, to, country_code
end
# Simple linear search
def linear_search(ip_addr_num)
IO.foreach(Filename) do |line|
from, to, country_code = parse_line(line)
if ip_addr_num >= from and ip_addr_num <= to
return "Linear Search: #{country_code}"
end
end
return "No Country Found"
end
# Binary search of data in memory
def binary_search(ip_addr_num)
fp = File.open(Filename)
data = fp.readlines
fp.close
low = 0
high = data.size
while (low <= high)
mid = (low + high) / 2
from, to, country_code = parse_line(data[mid])
if (from == nil or from > ip_addr_num)
high = mid - 1
elsif (to < ip_addr_num)
low = mid + 1
else
return "Binary Search: #{country_code}"
end
end
return "No Country Found"
end
# Binary Seek Search
# The fastest method implemented here is to use a binary search to seek through the file
# Takes advantage of the fact that all ip ranges are ordered in the file
def binary_seek(ip_addr_num)
low = 0
high = File.size(Filename)
fp = File.open(Filename)
# Find first line of real data and set "low" placeholder accordingly
line = fp.gets
while line.strip.size == 0 or line[0].chr == "#"
line = fp.gets
low = low + line.size
end
# Then find the corresponding the IP Range, if any
while (low <= high)
mid = (low + high) / 2
fp.seek(mid, IO::SEEK_SET)
line = fp.gets # read once to find the next line
line = fp.gets # read again to get the next full line
from, to, country_code = parse_line(line)
if (from == nil or from > ip_addr_num) # Safety check
high = mid - 1
elsif (to < ip_addr_num)
low = mid + 1
else
fp.close
return "Binary Seek Search: #{country_code}"
end
end
fp.close
return "No Country Found"
end
for ip in ARGV
puts binary_seek(convert_ip_str_to_num(ip))
end