Source: utils.js

/** ****************************************************************************************************
 * File: utils.js
 * Project: geohash
 * @author Nick Soggin <iSkore@users.noreply.github.com> on 19-Feb-2019
 *******************************************************************************************************/
'use strict';

const
	{
		ENCODE_AUTO,
		MIN_LNG,
		MIN_LAT,
		MAX_LNG,
		MAX_LAT
	} = require( './variables' );

/**
 * isNumber
 * @description
 * determine if value is a valid number
 * @param {number} n - value
 * @return {boolean} - true if value is a number
 */
function isNumber( n ) {
	return n === n && n === +n;
}

/**
 * clamp
 * @description
 * basic number clamp
 * @param {number} n - number to clamp
 * @param {number} min - minimum value
 * @param {number} max - maximum value
 * @return {number|*} - clamped value
 */
function clamp( n, min, max ) {
	return Math.min( Math.max( min, n ), max );
}

/**
 * longitudeClamp
 * @description
 * Determines if longitude is in the realm of possibility
 * If it's not, clamp the longitude value to the MIN/MAX
 * @param {number} lng - longitude
 * @returns {number} - longitude
 */
function longitudeClamp( lng ) {
	if ( !isNumber( lng ) ) {
		throw new Error( 'number required for `lng`' );
	}

	return clamp( lng, MIN_LNG, MAX_LNG );
}

/**
 * latitudeClamp
 * @description
 * Determines if latitude is in the realm of possibility
 * If it's not, clamp the latitude value to the MIN/MAX
 * @param {number} lat - latitude
 * @returns {number} - latitude
 */
function latitudeClamp( lat ) {
	if ( !isNumber( lat ) ) {
		throw new Error( 'number required for `lat`' );
	}

	return clamp( lat, MIN_LAT, MAX_LAT );
}

/**
 * clampRelative
 * @description
 * basic relative value clamp
 * @param {number} n - number to clamp
 * @param {number} min - minimum value
 * @param {number} max - maximum value
 * @return {number|*} - clamped value or offset
 */
function clampRelative( n, min, max ) {
	return isNumber( n ) ?
		n > max ? min + n % max :
			n < min ? max + n % max :
				n : n;
}

/**
 * longitudeClampRelative
 * @description
 * Determines if longitude is in the realm of possibility
 * If it's not, return the longitude position relative to the amount offset
 * @param {number} lng - longitude
 * @returns {number} - longitude
 */
function longitudeClampRelative( lng ) {
	if ( !isNumber( lng ) ) {
		throw new Error( 'number required for `lng`' );
	}

	return clampRelative( lng, MIN_LNG, MAX_LNG );
}

/**
 * latitudeClampRelative
 * @description
 * Determines if latitude is in the realm of possibility
 * If it's not, return the latitude position relative to the amount offset
 * @param {number} lat - latitude
 * @returns {number} - latitude
 */
function latitudeClampRelative( lat ) {
	if ( !isNumber( lat ) ) {
		throw new Error( 'number required for `lat`' );
	}

	return clampRelative( lat, MIN_LAT, MAX_LAT );
}

/**
 * determinePrecision
 * @description
 * Estimate what precision to use based on the input longitude/latitude
 *
 * @param {number} lng - longitude
 * @param {number} lat - latitude
 * @param {number?} [precision=ENCODE_AUTO] - precision override
 * @returns {number} - precision estimate
 */
function determinePrecision( lng, lat, precision = ENCODE_AUTO ) {
	if ( !isNumber( precision ) ) {
		throw new Error( 'number required for `precision`' );
	}

	if ( precision === ENCODE_AUTO ) {
		if ( !isNumber( lng ) ) {
			throw new Error( 'number required for `lng`' );
		}
		else if ( !isNumber( lat ) ) {
			throw new Error( 'number required for `lat`' );
		}

		lng = longitudeClampRelative( lng );
		lat = latitudeClampRelative( lat );

		if ( ~~lat === lat && ~~lng === lng ) {
			precision = 0;
		}
		else {
			const
				latLen  = +( lat.toString( 10 ).length ),
				lngLen  = +( lng.toString( 10 ).length ),
				average = ( latLen + lngLen ) / 2;

			precision = average >= 12 ? 12 : average;
		}
	}

	return ~~precision;
}

function determineDirection( [ x, y ] ) {
	if ( !isNumber( x ) ) {
		throw new Error( 'number required for `x`' );
	}
	else if ( !isNumber( y ) ) {
		throw new Error( 'number required for `y`' );
	}
	else if ( x === 0 && y === 0 ) {
		return 'c';
	}
	else if ( !( x ^ y ) ) {
		return ( x & y ) === 1 ? 'ne' : 'sw';
	}
	else if ( !!( x & y ) ) {
		return x === 1 ? 'se' : 'nw';
	}
	else if ( ( x | y ) > 0 ) {
		return !!x ? 'e' : 'n';
	}
	else {
		return !!x ? 'w' : 's';
	}
}

module.exports = {
	isNumber,
	clampRelative,
	longitudeClamp,
	latitudeClamp,
	longitudeClampRelative,
	latitudeClampRelative,
	determinePrecision,
	determineDirection
};