geohashing 0.1.0

A geohashing implementation
Documentation
//! [![The original comic](https://imgs.xkcd.com/comics/geohashing.png)](https://xkcd.com/426/)

extern crate md5;
extern crate chrono;
use chrono::{Datelike, NaiveDate};
#[cfg(feature="online")]
extern crate reqwest;
#[cfg(feature="online")]
extern crate regex;
#[cfg(feature="online")]
use regex::Regex;

fn bytes_to_f64(hex: &[u8]) -> f64 {
	let mut curvalue = 0.0;

	for (i, c) in hex.iter().enumerate() {
		// first "hex char"
		let mut d1 = (c / 16) as f64;
		let mut d2: f64 = 16.0;
		d2 = d2.powf((i * 2 + 1) as f64);
		curvalue = curvalue + (d1 / d2);

		// second "hex char"
		d1 = (c % 16) as f64;
		d2 = 16.0;
		d2 = d2.powf((i * 2 + 2) as f64);
		curvalue = curvalue + (d1 / d2);
	}

	curvalue
}

#[cfg(feature="offline")]
const DJIA: [((u64, u8, u8), &str); 32785] = include!("djia.txt");

#[cfg(feature="offline")]
fn search_djia_offline(date: NaiveDate) -> Option<&'static str> {
	let year = date.year();
	let month = date.month();
	let day = date.day();
	let date = (year as u64, month as u8, day as u8);
	DJIA.binary_search_by_key(&date, |x| x.0).ok().map(|idx| DJIA[idx].1)
}

#[cfg(feature="online")]
fn search_djia_online(date: NaiveDate) -> Option<String> {
	let year = date.year();
	let month = date.month();
	let day = date.day();
	let client = reqwest::Client::new();

	let djia_format = Regex::new(r#"^\d+\.\d+$"#).unwrap();

	let url = format!("http://geo.crox.net/djia/{}/{:02}/{:02}", year, month, day);
	if let Some(djia) = client.get(&url).send().map(|mut x| x.text().ok()).ok().unwrap_or(None) {
		if djia_format.is_match(&djia) {
			return Some(djia);
		}
	}

	let url = format!("http://carabiner.peeron.com/xkcd/map/data/{}/{:02}/{:02}", year, month, day);
	
	if let Some(djia) = client.get(&url).send().map(|mut x| x.text().ok()).ok().unwrap_or(None) {
		if djia_format.is_match(&djia) {
			return Some(djia);
		}
	}

	None
}

#[derive(Debug)]
pub enum Error {
	#[cfg(feature="offline")]
	NotInOfflineData,
	#[cfg(feature="online")]
	NetworkError,
	#[cfg(not(any(feature="online", feature="offline")))]
	NoDataSource
}

#[allow(unreachable_code)]
pub fn search_djia(date: NaiveDate) -> Result<String, Error> {
	#[cfg(feature="offline")]
	{
		if let Some(djia) = search_djia_offline(date) {
			return Ok(djia.to_owned());
		}
	}
	#[cfg(feature="online")]
	{
		if let Some(djia) = search_djia_online(date) {
			return Ok(djia.to_owned());
		} else {
			return Err(Error::NetworkError);
		}
	}
	#[cfg(feature="offline")]
	{
		return Err(Error::NotInOfflineData);
	}

	#[cfg(not(any(feature="online", feature="offline")))]
	{
		Err(Error::NoDataSource)
	}
}

/// Get the coordinates of the geohash on that day. Fully W30 compliant.
pub fn get_geohash(date: NaiveDate, (lat, lon): (f64, f64)) -> Option<(f64, f64)> {
	let djia = if lon > -30.0 && date >= NaiveDate::from_ymd(2008, 5, 27) {
		search_djia(date.pred()).ok()
	} else {
		search_djia(date).ok()
	}?;

	let (lat_offset, lon_offset) = djia_to_hash(date, &djia);
	if lon < 0.0 {
		Some((lat+lat_offset, lon-lon_offset))
	} else {
		Some((lat+lat_offset, lon+lon_offset))
	}
}

/// Get the coordinates of the globalhash on that day.
pub fn get_globalhash(date: NaiveDate) -> Option<(f64, f64)> {
	let djia = search_djia(date.pred()).ok()?;

	Some(djia_to_globalhash(date, &djia))
}

pub fn djia_to_hash(date: NaiveDate, djia: &str) -> (f64, f64) {
	let md5 = md5::compute(format!("{}-{:02}-{:02}-{}", date.year(), date.month(), date.day(), djia));

	(bytes_to_f64(&md5[0..8]), bytes_to_f64(&md5[8..]))
}

pub fn djia_to_globalhash(date: NaiveDate, djia: &str) -> (f64, f64) {
	let md5 = md5::compute(format!("{}-{:02}-{:02}-{}", date.year(), date.month(), date.day(), djia));

	(bytes_to_f64(&md5[0..8]) * 180.0 - 90.0, bytes_to_f64(&md5[8..]) * 360.0 - 180.0)
}

/*
fn main() {
	let target_lat = 0.00579;
	let target_lon = 0.38112;
	let mut min_dist = 10000.0;
	let mut min_date = String::new();
	let mut stdout = std::io::stdout();
	for year in 2018..=2018 {
		for month in 7..=7 {
			for day in 4..=4 {
				let date = format!("{}-{:02}-{:02}", year, month, day);

				let djia = search_djia((year, month, day));
				if djia.is_err() {
					continue;
				}
				let djia = djia.unwrap();

				let md5 = md5::compute(format!("{}-{}", date, djia));

				let lat = bytes_to_f64(&md5[0..8]);
				let lon = bytes_to_f64(&md5[8..]);

				let dist = ((target_lat - lat).powf(2.0) + (target_lon - lon).powf(2.0)).sqrt();

				//stdout.write_all(&[b'.']).unwrap();
				//stdout.flush().ok().expect("Could not flush stdout");
				if dist < min_dist {
					min_dist = dist;
					println!();
					println!("{} - {} - {} {} - {:?}", date, dist, lat, lon, djia);
					min_date = date;
				}
			}
		}
	}
	println!();
	println!("-> {}", min_date);
	//println!("</Document></kml>");
}
*/