Source: scripts/utils/dateHandler.js

/**
 * Class for dealing with times and dates.
 */
class DateHandler {
	/**
	 * Returns a timestamp for the current time.
	 * 
	 * @return {number} A timestamp of the current date in seconds.
	 */
	static getCurrentTimestamp() {
		// Divide milliseconds by 1000 to get seconds.
		return Math.floor(Date.now() / 1000);
	}

	/**
	 * Converts a timestamp in seconds into a date in string format.
	 * 
	 * @param {number} ts The timestamp (in seconds) which we want to convert.
	 * @return {string} The formatted date (dd.mm.yyyy).
	 */
	static timestampToString(ts) {
		let date = new Date(ts * 1000); // Multiply by 1000 to get milliseconds from seconds.
		
		let day = date.getDate().toString().padStart(2, '0');
		let month = (date.getMonth() + 1).toString().padStart(2, '0'); // Month is zero indexed
		let year = date.getFullYear().toString();
		
		return `${day}.${month}.${year}`;
	}

	/**
	 * Makes sure that a given timestamp is not already used (unique).
	 * If it is already in use, we add iteratively one second to it until it is unique.
	 * 
	 * @param {number} ts The original timestamp.
	 * @param {Storage} storage A storage object for accessing the data.
	 * @return {number} A unique timestamp generated from the original timestamp.
	 */
	static createUniqueTimestamp(ts, storage) {
		let content = storage.getData(DateHandler.timestampToFilename(ts), {
			connector: 'or',
			params: [['type', 'earning'], ['type', 'spending']]
		});

		// A single for-loop is sufficient because the file is sorted by date.
		// So if we add one, we can detect new duplicates in the following iterations
		// (and add one again until no duplicates are left).
		for (let i = 0; i < content.length; i++) {
			if (content[i].date === ts) {
				ts = ts + 1;
			}
		}

		return ts;
	}

	/**
	 * Creates a filename for a given date in string format.
	 * Input format: dd.mm.yyyy, output format: mm.yyyy.json
	 * 
	 * @param {string} date The date which should be reversed.
	 * @return {string} The filename for the given date in mm.yyyy.json format.
	 */
	static strDateToFilename(date) {
		let dSplit = date.split('.');

		return `${dSplit[1]}.${dSplit[2]}.json`;
	}

	/**
	 * Creates a filename for a given timestamp.
	 * Input: Timestamp in seconds, output format: mm.yyyy.json
	 * 
	 * @param {number} ts The timestamp for which we want to obtain the filename.
	 * @return {string} The filename for the given timestamp in mm.yyyy.json format.
	 */
	static timestampToFilename(ts) {
		return DateHandler.strDateToFilename(DateHandler.timestampToString(ts));
	}

	/**
	 * Creates a timestamp out of a given day, month and year.
	 * 
	 * @param {string} day The day of the date.
	 * @param {string} month The month of the date.
	 * @param {string} year The year of the date.
	 * @return {number} The corresponding timestamp for the given date.
	 */
	static dateToTimestamp(day, month, year) {
		return (new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`)).getTime() / 1000;
	}

	/**
	 * Increments a given date by a given interval.
	 * 
	 * @param {number} startDate The original date where the transaction started.
	 * Necessary to keep the original day.
	 * @param {number} oldDate The date to increment as a timestamp in seconds.
	 * @param {number} interval The interval, in form of one of the following:
	 * 0 => one week (7 days),
	 * 1 => four weeks (28 days),
	 * 2 => one month,
	 * 3 => two months,
	 * 4 => three months (quarterly),
	 * 5 => six months (biannual),
	 * 6 => one year (annual)
	 */
	static stepInterval(startDate, oldDate, interval) {
		interval = parseInt(interval);

		if (interval === 0 || interval === 1) {
			return DateHandler.stepIntervalDays(oldDate, interval);
		} else if (interval >= 2 && interval <= 6) {
			return DateHandler.stepIntervalMonths(startDate, oldDate, interval);
		} else {
			throw new Error(`Invalid interval! Given ${interval} but must be between 0 and 6.`);
		}
	}

	/**
	 * Increments a given date according to a given interval.
	 * 
	 * @param {number} oldDate The date to increment as a timestamp in seconds.
	 * @param {number} interval The interval: Can be either 0 for one week or 1 for four weeks.
	 * @return {number} The new data as a timestamp in seconds.
	 */
	static stepIntervalDays(oldDate, interval) {
		interval = parseInt(interval);

		if (interval !== 0 && interval !== 1) {
			throw new Error(`Invalid interval! Given ${interval} but expected 0 or 1.`);
		}

		let weekInSeconds = 7 * 24 * 60 * 60;
		let fourWeeksInSeconds = 4 * weekInSeconds;
		
		return oldDate + (interval === 0 ? weekInSeconds : fourWeeksInSeconds);
	}

	/**
	 * Increments a given date according to a given interval.
	 * 
	 * @param {number} startDate The original date where the transaction started.
	 * Necessary to keep the original day.
	 * @param {number} oldDate The date to increment as a timestamp in seconds.
	 * @param {number} interval The interval: Can be either 2 for one month, 3 for two months,
	 * 4 for three moths, 5 for six months or 6 for one year.
	 * @return {number} The new data as a timestamp in seconds.
	 */
	static stepIntervalMonths(startDate, oldDate, interval) {
		interval = parseInt(interval);
		
		if (interval < 2 || interval > 6) {
			throw new Error(`Invalid interval! Given ${interval} but must be between 2 and 6.`);
		}

		let strDateArr = DateHandler.timestampToString(oldDate).split('.'); // ['dd', 'mm', 'yyyy']
		// Keep the original day (e.g. from 31.03 to 30.04 to 31.05).
		strDateArr[0] = (new Date(startDate * 1000)).getDate().toString().padStart(2, '0');

		switch (interval) {
			case 2: // Increase by one month
				strDateArr[1] = (parseInt(strDateArr[1]) + 1).toString().padStart(2, '0');
				break;
			case 3: // Increase by two months
				strDateArr[1] = (parseInt(strDateArr[1]) + 2).toString().padStart(2, '0');
				break;
			case 4: // Increase by three months
				strDateArr[1] = (parseInt(strDateArr[1]) + 3).toString().padStart(2, '0');
				break;
			case 5: // Increase by six months
				strDateArr[1] = (parseInt(strDateArr[1]) + 6).toString().padStart(2, '0');
				break;
			case 6: // Increase by one year
				strDateArr[2] = (parseInt(strDateArr[2]) + 1).toString().padStart(2, '0');
				break;
		}

		// Enter a new year if month overflowed
		let month = parseInt(strDateArr[1]);
		if (month > 12) {
			strDateArr[1] = (month % 12).toString().padStart(2, '0');
			strDateArr[2] = (parseInt(strDateArr[2]) + parseInt(month / 12)).toString().padStart(2, '0');
		}

		// Set day to the last day of the month if it overflows (e.g. from 31.03 to 31.04).
		if (parseInt(strDateArr[0]) > (new Date(strDateArr[2], strDateArr[1], 0)).getDate()) {
			strDateArr[0] = (new Date(strDateArr[2], strDateArr[1], 0)).getDate().toString().padStart(2, '0');
		}

		return Math.floor((new Date(strDateArr.reverse().join('-'))).getTime() / 1000);
	}

	/**
	 * Checks whether d1 <= d2, i.e., whether d2 is the same day as d1 or d2
	 * comes after d1 (only day, month, year are relevant, the actual time of
	 * the day is neglected).
	 * 
	 * @param {Date} d1 The first date.
	 * @param {Date} d2 The second date.
	 * 
	 * @return {bool} True if d1 <= d2 (in terms of day, month, year), else false.
	 */
	static lessEqualDate(d1, d2) {
		if (d1.getFullYear() < d2.getFullYear()) {
			return true;
		} else if (d1.getFullYear() > d2.getFullYear()) {
			return false;
		}

		if (d1.getMonth() < d2.getMonth()) {
			return true;
		} else if (d1.getMonth() > d2.getMonth()) {
			return false;
		}

		return d1.getDate() <= d2.getDate();
	}
}

module.exports = DateHandler;