const fs = require('fs');
const path = require('path');
const Table = require('cli-table3');
// === Pure functions ===
/** @constant {string[]} */
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
/**
* Parses structured text input from a file into usable data.
*
* @param {string} input - Raw text input.
* @returns {{ refillData: Object<string, number>, consumption: number, initialCapacity: number }}
*/
function parseInputText(input) {
const lines = input.split(/\r?\n/); //use built-in menthod to split the input text to strings
const refillData = {}; // stores the refill volume by months in array
// Default values — will be overwritten if specified in the input
let consumption = 2000;
let initialCapacity = 2000;
// Iterate through the lines
for (const line of lines) {
//Remove comments (everything after #), trim whitespace and skip empty lines
const cleanLine = line.split('#')[0].trim();
if (!cleanLine) continue;
// Try to split the line into a key and value pair by colon
const [keyRaw, valueRaw] = cleanLine.split(':');
if (!keyRaw || !valueRaw) continue; // Skip malformed lines
// Normalize the key to lowercase to make comparison case-insensitive
const key = keyRaw.trim().toLowerCase();
// Try to convert the value to a number
const value = parseInt(valueRaw.trim(), 10);
// Handle special keys for consumption and tank volume
if (key === 'consumption') {
consumption = value;
} else if (key === 'initial tank volume') {
initialCapacity = value;
} else {
// Otherwise, treat the key as a month name (preserve original capitalization)
const month = keyRaw.trim();
if (MONTHS.includes(month)) {
refillData[month] = value;
}
}
}
return { refillData, consumption, initialCapacity };
}
/**
* Simulates monthly tank usage, calculating volume changes and deficits.
*
* @param {Object<string, number>} refillData - Monthly refill volumes.
* @param {number} consumption - Amount withdrawn every even month.
* @param {number} startingVolume - Volume at the start of the year.
* @param {number} maxCapacity - Maximum capacity of the tank.
* @returns {{
* table: Array<{ month: string, begin: number, withdrawn: number, received: number, end: number, deficit: string | number }>,
* finalVolume: number,
* maxDeficit: number,
* hasDeficit: boolean
* }}
*/
function calculateTable(refillData, consumption, startingVolume, maxCapacity) {
const results = []; // will hold the resulting array
let volume = startingVolume;
let maxDeficit = 0; //will hold the largest deficit across all months
// Iterate through all 12 months
for (let i = 0; i < MONTHS.length; i++) {
const month = MONTHS[i];
const received = refillData[month] ?? 0;
const begin = volume; // holds initial volume for this month
let withdrawn = 0; // How much was withdrawn this month
let deficit = '—'; // Default deficit is none
// 1) Withdrawal
const isEvenMonth = (i + 1) % 2 === 0;
if (isEvenMonth) {
withdrawn = consumption;
if (volume < consumption) {
const shortfall = consumption - volume;
deficit = '-' + shortfall;
maxDeficit = Math.max(maxDeficit, shortfall); // update tracked deficit
withdrawn = volume; // Only withdraw what is available
volume = 0;
} else {
volume -= consumption; // Sufficient water – subtract
}
}
//2) Refilling
volume += received;
const surplus = Math.max(0, volume - maxCapacity); // If overfilled, compute surplus. Not used
volume = Math.min(volume, maxCapacity); // Cap at max capacity
// Store result for this month
results.push({
month,
begin,
withdrawn,
received,
end: volume,
deficit,
});
}
// Return full result structure
return {
table: results, // Array of month summaries
finalVolume: volume,
maxDeficit,
hasDeficit: results.some(row => row.deficit !== '—') // True if any month had deficit
};
}
/**
* Uses binary search to find the minimal tank volume required to avoid any deficit.
*
* @param {Object<string, number>} refillData - Monthly refill volumes.
* @param {number} consumption - Amount withdrawn every even month.
* @returns {number|null} - Minimum sufficient volume or null if impossible.
*/
function findMinimumCapacity(refillData, consumption) {
let low = consumption; //setting the lower search limit
let high = 10000; //setting the upper search limit
let answer = null; //Best result found so far
// Binary search loop
while (low <= high) {
const mid = Math.floor((low + high) / 2); // Try the midpoint capacity
const result = calculateTable(refillData, consumption, mid, mid); //Year evaluation with the starting and max tank level = mid
if (!result.hasDeficit) {
answer = mid; //stores mid as a possible result
high = mid - 1; // next search in a smaller solution space
} else {
low = mid + 1; //next search in a bigger solution space
}
}
return answer;
}
/**
* Converts table data into a formatted ASCII table.
*
* @param {Array<Object>} tableData - Array of row objects with month info.
* @returns {string} - ASCII-formatted table string.
*/
function getTableString(tableData) {
//Instantiation of an object of the class 'Table' from 'cli-table3' using the constructor
//Setting up its columns
const table = new Table({
head: ['Month', 'Beginning', 'Discharge', 'Inlet', 'End', 'Deficit'],
colWidths: [8, 12, 12, 8, 8, 10],
});
// Call the 'row' function for each tableData array element
tableData.forEach(row => {
// 'cli-table3' method .push adds an array of values as a row in a table.
table.push([
row.month,
row.begin,
row.withdrawn,
row.received,
row.end,
row.deficit,
]);
});
// 'cli-table3' method .toString() returns all text rows formatted as ASCII-table
return table.toString();
}
// === Impure functions ===
/**
* Reads input.txt file and parses it into usable data.
*
* @param {string} filePath - Path to the input.txt file.
* @returns {{ refillData: Object<string, number>, consumption: number, initialCapacity: number }}
*/
function parseInputFile(filePath) {
// read and convert the file into the string;
// write the string to the "input"
const input = fs.readFileSync(filePath, 'utf-8');
// call the parseInputText function to parse the string into the data structure
// returns the object {refillData, consumption, initialCapacity}
return parseInputText(input);
}
/**
* Prints a table with a title to the console.
*
* @param {string} title - Table title.
* @param {Array<Object>} tableData - Table rows.
*/
function printTable(title, tableData) {
console.log(title);
console.log(getTableString(tableData));
}
/**
* Entry point for the script. Parses input, calculates and prints tables.
*/
function main() {
// writing the full path to the 'input.txt' to the inputPath variable
const inputPath = path.join(__dirname, 'input.txt');
// writing of the input data to three variables by destructing the object returned by parseInputFile
const { refillData, consumption, initialCapacity } = parseInputFile(inputPath);
// Calculation of the first table and writing the result to "firstRun"
// result is stored as an array of month rows with data
const firstRun = calculateTable(refillData, consumption, initialCapacity, initialCapacity);
// Output of the first table
printTable('\nDEFICIT IDENTIFICATION:', firstRun.table);
console.log(`Note: withdrawal occurs before topping up.`);
console.log(`Note: optimal volume is determined by the binary search method.`);
// Call for binary search function. Returns either the desired value or null
const optimalCapacity = findMinimumCapacity(refillData, consumption);
// Returns 10000 if optimalCapacity was not found (null)
const finalCapacity = optimalCapacity ?? 10000;
// Calculation of the second table and writing the result to "secondRun"
const secondRun = calculateTable(refillData, consumption, finalCapacity, finalCapacity);
// Output of the second table
printTable('\nTEST ITERATION WITH OPTIMAL VOLUME:', secondRun.table);
if (optimalCapacity === null) {
console.log(`\n❌ Even with the maximum tank volume (10,000 liters), it is impossible to avoid a deficit.`);
console.log(`ℹ️ The manual refilling will be required.`);
} else {
console.log(`Minimum sufficient volume of the tank: ${optimalCapacity} liters`);
}
}
main();