(function (define) {
'use strict';
/**
* @module CurrencyConverter
* @description JavaScript currency converter
*/
define(['jquery'], function ($) {
return (function (settings) {
var HOURS = 60 * 60 * 1000,
DEFAULTS = {
RATES_VALIDITY_HOURS : 24,
CACHE_TO_LOCAL_STORAGE : false,
LOCAL_STORAGE_VARIABLE_NAME: 'JS_CURRENCY_CONVERTER_CACHED_RATES',
API: {
url: 'http://free.currencyconverterapi.com/api/v3/convert',
queryParams: {
compact: 'y',
apiKey: ''
}
}
},
SETTINGS = $.extend(true, DEFAULTS, settings),
CACHED_RATES = {},
CONVERSIONS_IN_PROGRESS = {};
var CurrencyConverter = {};
// MAIN FUNCTIONS
CurrencyConverter.config = config;
CurrencyConverter.getConfig = getConfig;
CurrencyConverter.getRate = getRate;
CurrencyConverter.fetchQuote = fetchQuote;
CurrencyConverter.convertAmount = convertAmount;
// API CONVERSION IN PROGRESS
CurrencyConverter.isConversionInProgress = isConversionInProgress;
CurrencyConverter.setConversionInProgress = setConversionInProgress;
CurrencyConverter.getConversionInProgress = getConversionInProgress;
// CACHE HELPERS
CurrencyConverter.cacheToLocalStorage = cacheToLocalStorage;
CurrencyConverter.cacheFromLocalStorage = cacheFromLocalStorage;
CurrencyConverter.isRateCachedAndNonExpired = isRateCachedAndNonExpired;
CurrencyConverter.isRateCached = isRateCached;
CurrencyConverter.isDateExpired = isDateExpired;
CurrencyConverter.cacheRate = cacheRate;
CurrencyConverter.getRateFromCache = getRateFromCache;
// UTILITIES
CurrencyConverter.toQuery = toQuery;
CurrencyConverter.isObject = isObject;
CurrencyConverter.setToLocalStorage = setToLocalStorage;
CurrencyConverter.getFromLocalStorage = getFromLocalStorage;
CurrencyConverter.isLocalStorageAvailable = isLocalStorageAvailable;
CurrencyConverter.buildUrl = buildUrl;
if(SETTINGS.CACHE_TO_LOCAL_STORAGE) {
CurrencyConverter.cacheFromLocalStorage();
}
return CurrencyConverter;
/**
* @function config
* @description Overrides default CurrencyConverter settings
* @param {object} settings Overrides CurrencyConverter settings object
* @property {number} settings.CACHE_TO_LOCAL_STORAGE Cache conversion rate to local storage, if available
* @property {number} settings.RATES_VALIDITY_HOURS Cached conversion rate validity in hours
* @property {string} settings.LOCAL_STORAGE_VARIABLE_NAME Variable name where the rates will be cached in local storage
* @property {object} settings.API object API configuration object
* @property {string} settings.API.url API Endpoint url
* @property {object} settings.API.queryParams Query parameters key pair values
* @property {string} settings.API.queryParams.apiKey API key for non-free version of the API
* @property {string} settings.API.queryParams.compact API response object type
*/
function config (options) {
if (CurrencyConverter.isObject(options)) {
SETTINGS = $.extend(SETTINGS, options);
}
}
/**
* @function getConfig
* @description Returns CurrencyConverter settings object
* @return {settings}
* @property {number} settings.CACHE_TO_LOCAL_STORAGE Cache conversion rate to local storage, if available
* @property {number} settings.RATES_VALIDITY_HOURS Cached conversion rate validity in hours
* @property {string} settings.LOCAL_STORAGE_VARIABLE_NAME Variable name where the rates will be cached in local storage
* @property {object} settings.API object API configuration object
* @property {string} settings.API.url API Endpoint url
* @property {object} settings.API.queryParams Query parameters key pair values
* @property {string} settings.API.queryParams.apiKey API key for non-free version of the API
* @property {string} settings.API.queryParams.compact API response object type
*/
function getConfig () {
return SETTINGS;
}
/**
* @function fetchQuote
* @description Returns conversion rate from the API
* @param {string} fromCurrency Currency converting from
* @param {string} toCurrency Currency converting to
* @return {Promise<number>} Resolves to conversion rate number
*/
function fetchQuote (fromCurrency, toCurrency) {
var deferred = $.Deferred();
var query = CurrencyConverter.toQuery(fromCurrency, toCurrency);
// If the call for the same converesion is in progress, return the same promise
if(CurrencyConverter.isConversionInProgress(query)) {
return CurrencyConverter.getConversionInProgress(query);
}
$.get(CurrencyConverter.buildUrl({q:query}))
.done(onSuccess)
.fail(onError)
.always(onAlways);
// cache the promise, in case it gets called while this one is in progress
CurrencyConverter.setConversionInProgress(query, deferred.promise());
return deferred.promise() ;
function onSuccess (response) {
// cache the result
CurrencyConverter.cacheRate(query, response[query].val, new Date());
deferred.resolve(response[query].val);
}
function onError (error) {
deferred.reject(error);
}
function onAlways () {
// dereference API call which was in progress
CurrencyConverter.setConversionInProgress(query, null);
}
}
/**
* @function getRate
* @description Returns conversion rate.
* If the conversion rate is already available in the cache, and not expired, that rate is used.
* If the conversion rate is not available in the cache, API rate fetch is attempted.
* If the rate is available in the cache but expired, API rate fetch is attempted.
* If the rate is available in the cache and expired, and API rate fetch fails, expired rate is returned if available.
* @param {string} fromCurrency Currency converting from
* @param {string} toCurrency Currency converting to
* @return {Promise<number>} Resolves to conversion rate number
*/
function getRate (fromCurrency, toCurrency) {
var deferred = $.Deferred();
var query = CurrencyConverter.toQuery(fromCurrency, toCurrency);
if(CurrencyConverter.isRateCachedAndNonExpired(query)) {
resolveRate(false, CACHED_RATES[query].value);
} else {
CurrencyConverter.fetchQuote(fromCurrency, toCurrency)
.done(fetchOnSuccess)
.fail(oldRateFallback);
}
return deferred.promise();
function fetchOnSuccess (rate) {
resolveRate(false, rate);
}
// if the api fails, try to return an expired rate as a failback
function oldRateFallback (error) {
// if rate is cached but expired, resolve old rate
if(CurrencyConverter.isRateCached(query)){
resolveRate(true, CACHED_RATES[query].value);
} else {
deferred.reject(error);
}
}
function resolveRate (isExpired, rateValue) {
deferred.resolve({
expired: isExpired,
rate: rateValue
});
}
}
/**
* @function convertAmount
* @description Converts given amount from given currency to given currency
* @param {number} amount Amount of money converting
* @param {string} fromCurrency Currency converting from
* @param {string} toCurrency Currency converting to
* @return {Promise<conversionObject>} Promise to the conversionObject
* @property {number} conversionObject.value converted amount
* @property {number} conversionObject.rate conversion rate
* @property {boolean} conversionObject.expired is the rate expired (considering RATES_VALIDITY_HOURS)
*/
function convertAmount (amount, fromCurrency, toCurrency) {
var deferred = $.Deferred();
CurrencyConverter.getRate(fromCurrency, toCurrency)
.done(onSuccess)
.fail(onError);
return deferred.promise() ;
function onSuccess (data) {
data.value = amount * data.rate;
deferred.resolve(data);
}
function onError (error) {
deferred.reject(error);
}
}
/**
* @function setConversionInProgress
* @description Sets given query and promise as a key:value pair in the private CONVERSIONS_IN_PROGRESS object
* @param {string} query Query RATETO_RATEFROM key - pair
* @param {Promise<conversionObject>} promise Conversion in progress promise
*/
function setConversionInProgress (query, promise) {
CONVERSIONS_IN_PROGRESS[query] = promise;
}
/**
* @function isConversionInProgress
* @description Returns boolean wether the API call for given query is in progress
* @return {Boolean}
* @param {string} query RATETO_RATEFROM string
*/
function isConversionInProgress (query) {
return Boolean(CurrencyConverter.getConversionInProgress(query));
}
/**
* @function getConversionInProgress
* @description Returns a promise in progress for the given query
* @return {Promise<conversionObject>} Promise to the conversionObject
* @param {string} query RATETO_RATEFROM string
*/
function getConversionInProgress (query) {
return CONVERSIONS_IN_PROGRESS[query];
}
/**
* @function isRateCached
* @description Returns boolean wether the conversion object for the given query is cached in memory
* @return {Boolean}
* @param {string} query RATETO_RATEFROM string
*/
function isRateCached (query) {
return CurrencyConverter.isObject(CACHED_RATES[query]);
}
/**
* @function isDateExpired
* @description Returns boolean wether the input date is lesser then the SETTINGS.RATES_VALIDITY_HOURS setting
* @return {Boolean}
* @param {string} date Date string
*/
function isDateExpired (dateString) {
// when the rate is fetched from local storage, the date is a String
var cachedRateDateTime = new Date(dateString).getTime();
var nowDateTime = new Date().getTime();
return (nowDateTime - cachedRateDateTime) > (SETTINGS.RATES_VALIDITY_HOURS * HOURS);
}
/**
* @function isRateCachedAndNonExpired
* @description Returns the combination of CurrencyConverter.isRateCached CurrencyConverter.isDateExpired functions
* @return {Boolean}
* @param {string} query RATETO_RATEFROM string
*/
function isRateCachedAndNonExpired (query) {
return CurrencyConverter.isRateCached(query)
&& !CurrencyConverter.isDateExpired(CACHED_RATES[query].date);
}
/**
* @function cacheRate
* @description Caches given rate to the memory. If SETTINGS.CACHE_TO_LOCAL_STORAGE is true, entire CACHED_RATES object will be cached to localStorage as well
* @return {undefined}
* @param {string} rateName Conversion rate name in RATETO_RATEFROM format. This is the key under which the conversion object will be mapped in the CACHED_RATES private object
* @param {number} value Conversion rate value
* @param {date} date Converesion rate caching date
*/
function cacheRate (rateName, value, date) {
CACHED_RATES[rateName] = {
value: value,
date: date
};
if(SETTINGS.CACHE_TO_LOCAL_STORAGE) {
CurrencyConverter.cacheToLocalStorage();
}
}
/**
* @function getRateFromCache
* @description Returns the conversion rate object form memory for the given query
* @return {conversionObject}
* @property {number} conversionObject.value converted amount
* @property {number} conversionObject.rate conversion rate
* @param {string} query RATETO_RATEFROM string
*/
function getRateFromCache (query) {
return CACHED_RATES[query];
}
/**
* @function cacheToLocalStorage
* @description Caches the private CACHED_RATES object to local storage
* @return {undefined}
*/
function cacheToLocalStorage () {
CurrencyConverter.setToLocalStorage(SETTINGS.LOCAL_STORAGE_VARIABLE_NAME, CACHED_RATES);
}
/**
* @function cacheFromLocalStorage
* @description Sets the private CACHED_RATES object to the value from localStorage
* @return {undefined}
*/
function cacheFromLocalStorage () {
CACHED_RATES = CurrencyConverter.getFromLocalStorage(SETTINGS.LOCAL_STORAGE_VARIABLE_NAME);
}
/**
* @function toQuery
* @description Returns the concatenated string of the two valued passed in, with underscore (_) as a concat character
* @return {string}
* @param {string} fromCurrency Rate we are converting from
* @param {string} toCurrency Rate we are converting to
*/
function toQuery (fromCurrency, toCurrency) {
return (fromCurrency || '') + '_' + (toCurrency || '');
}
/**
* @function isObject
* @description Returns boolean wether the passed in variable is an object or not
* @return {Boolean}
* @param {object} value Object we are testing
*/
function isObject(value) {
return value !== null && typeof value === 'object';
}
/**
* @function setToLocalStorage
* @description Sets given key:value pair to the local storage
* @return {undefined}
* @param {key} key Key for the given value
* @param {value} value Value for the given key
*/
function setToLocalStorage (key, value) {
if (CurrencyConverter.isLocalStorageAvailable()) {
localStorage.setItem(key, JSON.stringify(value));
} else {
console.error('Caching rates to local storage failed. Local storage not available');
}
}
/**
* @function getFromLocalStorage
* @description Retrieves a value from the local storage for the given key. On error returns empty object
* @return {object}
* @param {key} key Key for the given value
*/
function getFromLocalStorage (key) {
if (CurrencyConverter.isLocalStorageAvailable()) {
try {
return JSON.parse(localStorage.getItem(key)) || {};
} catch (e) {
return {};
}
} else {
console.error('Retrieving rates from local storage failed. Local storage not available');
}
}
/**
* @function isLocalStorageAvailable
* @description Tests the existence of the localStorage object on the global object
* @return {Boolean}
*/
function isLocalStorageAvailable(){
var test = 'js-currency-test';
try {
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch(e) {
return false;
}
}
/**
* @function buildUrl
* @description Builds API endpoint url from SETTINGS.API.url, SETTINGS.API.queryParams, and query parameters passed in
* @return {string}
* @param {object} queryParams Query parameter key pair values
*/
function buildUrl (queryParams) {
var url = SETTINGS.API.url + '?';
var params = $.extend({}, SETTINGS.API.queryParams, queryParams);
$.each(params, function (key, value, i) {
url += key + '=' + value + '&';
});
return url;
}
});
});
}(typeof define === 'function' && define.amd ? define : function (deps, factory) {
/* istanbul ignore next */
if (typeof module !== 'undefined' && module.exports) {
module.exports = factory(require('jquery'));
} else {
window.CurrencyConverter = factory(window.jQuery);
}
}));