[\\s\\S]*?', 'gi');
var oldHtml;
do {
oldHtml = html;
html = html.replace(tagOrComment, '');
} while (html !== oldHtml);
return html.replace(/ 3) {
BadCell(aCell);
return true;
}
// if only 2 numbers provided and both are less than 2 digits, assume MM-DD so prefix YYYY
if (NumArr.length == 2) {
if (NumArr[0].length < 3 && NumArr[1].length < 3) {
NumArr[2] = NumArr[1];
NumArr[1] = NumArr[0];
NumArr[0] = Date.today().toString('yyyy-MM-dd');
} else {
BadCell(aCell);
return true;
}
}
// if 3rd number is 4 digits, assume year, move to front
if (NumArr[2].length == 4) {
tmpnum = NumArr[0];
NumArr[0] = NumArr[2];
NumArr[2] = NumArr[1];
NumArr[1] = tmpnum;
}
// if 2nd number is 1 digit, pad w/ 0
if (NumArr[1].length == 1) {
NumArr[1] = "0" + NumArr[1];
}
// if 3rd number is 1 digit, pad w/ 0
if (NumArr[2].length == 1) {
NumArr[2] = "0" + NumArr[2];
}
// if numbers don't match expected length, badcell
if (NumArr[0].length != 4 || NumArr[1].length != 2 || NumArr[2].length != 2) {
BadCell(aCell);
return true;
}
// rebuild number.
aCell.innerHTML = NumArr[0] + "-" + NumArr[1] + "-" + NumArr[2];
OkCell(aCell);
return true;
}
if (aCell.cellIndex == 2) { // name col
if (aCellVal === "") {
BadCell(aCell);
} else {
if (aCellVal.split(",").length - 1 > 0) {
var cleaned = aCellVal.replace(/,/g, ';');
aCell.innerHTML = cleaned;
}
OkCell(aCell);
}
return true;
}
if (aCell.cellIndex == 3) { // freq col
if (aCellVal === "") {
OkCell(aCell);
return true;
}
CellInt = aCellVal.match(/^[0-9]*/);
CellTxt = aCellVal.match(/[wWmMyYdD]$/);
if (CellInt === "" || CellTxt === "" || CellInt + CellTxt != aCellVal) {
BadCell(aCell);
return true;
}
// var LowerVal = aCellVal.toLowerCase();
aCell.innerHTML = LowerVal;
OkCell(aCell);
return true;
}
if (aCell.cellIndex >= 4) { // debit and credit cols
if (aCellVal !== "" && isNaN(aCellVal)) {
BadCell(aCell);
return true;
}
if (aCellVal !== "") {
aCell.innerHTML = Number(parseFloat(aCellVal)).toFixed(2).toString();
}
OkCell(aCell);
return true;
}
}
function CPChanged(aCell) { // not working for non-load data events. boo.
aRow = aCell.parentNode;
CellBG = aRow.cells[6].getElementsByClassName('cp')[0].value;
for (var c = 0; c < aRow.cells.length - 1; c++) {
aRow.cells[c].bgColor = CellBG;
}
aRow.cells[6].bgColor = "#000000";
// alert(aCell.parentNode.constructor.name);
}
function MakeCell(aRow, numCell, innerText, Editable) {
Editable = Editable || "true";
innerText = innerText || "";
var aCell = aRow.insertCell(numCell);
if (numCell <= 5) {
aCell.innerHTML = innerText;
aCell.setAttribute('contenteditable', Editable);
aCell.addEventListener("blur", ValidateCell);
aCell.addEventListener("focus", SelectCellText);
aCell.addEventListener("click", SelectCellText);
if (numCell == 2) aCell.className = "left";
if (numCell >= 4) aCell.className = "right";
}
if (numCell == 6) {
DelOption = "
x
";
aCell.className = "left";
// create input element to append
var CPSel = document.createElement("input");
CPSel.setAttribute("type", "color");
CPSel.setAttribute("class", "cp");
if (innerText !== "") {
CPSel.setAttribute("value", innerText);
} else {
CPSel.setAttribute("value", "#F5F5F5");
}
aCell.appendChild(CPSel);
// all so that we can add the onchange element and have it register as an event
CPSel.oninput = CPChanged(aCell);
// moving on with brute force/easier element additions
aCell.innerHTML = aCell.innerHTML + "
▲
";
aCell.innerHTML = aCell.innerHTML + "
▼
";
aCell.innerHTML = aCell.innerHTML + "
x
";
}
}
function AddRow(LineArr) {
LineArr = LineArr || ["", "", "", "", "", "", ""];
var IOTable = document.getElementById("InOutTableBody");
var NewRow = IOTable.insertRow(IOTable.rows.length);
for (var x = 0; x < 7; x++) {
MakeCell(NewRow, x, LineArr[x]);
}
}
function SaveTable() {
BuildDataToSave();
// better filename - not from above
var now = new Date();
var day = ("0" + now.getDate()).slice(-2);
var month = ("0" + (now.getMonth() + 1)).slice(-2);
var today = now.getFullYear() + "-" + (month) + "-" + (day);
var FileName = "finances-" + today + ".csv";
// better file download mech - not from above. probably doesn't support IE. Good.
var a = document.createElement('a');
a.href = 'data:application/csv;charset=utf-8,' + encodeURIComponent(buildData);
a.target = '_blank';
a.download = FileName;
a.click();
}
function ClearTable(WarnText) {
var IOTable = document.getElementById("InOutTableBody");
if (typeof WarnText !== 'undefined' && IOTable.hasChildNodes() && document.getElementById('nosafe').checked == false) {
var yeswhoops = confirm(WarnText);
if (yeswhoops === false) {
return;
}
}
while (IOTable.hasChildNodes()) {
IOTable.removeChild(IOTable.firstChild);
}
ClearForecastTable();
document.getElementById("LoadInfo").innerHTML = "";
document.getElementById("StartDate").value = "";
document.getElementById("EndDate").value = "";
document.getElementById("StartBal").value = "";
}
function LoadTable(DataIn) {
ClearTable();
var DataLinesArr = DataIn.split(/\r\n|\n/);
for (var i = 0; i < DataLinesArr.length; i++) {
if (DataLinesArr[i] !== "") {
var DataLineArr = DataLinesArr[i].split(',');
if (DataLineArr[2] == "DEFINE-STARTS") {
document.getElementById("StartDate").value = DataLineArr[0];
document.getElementById("EndDate").value = DataLineArr[1];
document.getElementById("StartBal").value = DataLineArr[3];
} else {
AddRow(DataLineArr);
}
}
}
// document.getElementById('files').innerHTML = document.getElementById('files').innerHTML;
}
function DeleteRow(DivObj) {
if (document.getElementById('nosafe').checked == false) {
var yeswhoops = confirm("Delete row: Are you sure?");
if (yeswhoops !== true) {
return true;
}
}
var RowIndex = DivObj.parentNode.parentNode.rowIndex;
DivObj.parentNode.parentNode.parentNode.deleteRow(RowIndex - 1);
}
function MoveRowUpDn(DivObj, Dir) {
var DivRow = DivObj.parentNode.parentNode;
var DivParent = DivRow.parentNode;
if (Dir == "up") {
var PrevRow = DivRow.previousSibling;
DivParent.removeChild(DivRow);
DivParent.insertBefore(DivRow, PrevRow);
} else if (Dir == "dn") {
var RowIndex = DivRow.rowIndex;
var NextRow = DivRow.nextSibling;
var RowCount = DivRow.parentNode.rows.length;
DivParent.removeChild(DivRow);
if (RowIndex < RowCount) {
if (NextRow.nextSibling) {
DivParent.insertBefore(DivRow, NextRow.nextSibling);
} else {
DivParent.appendChild(DivRow);
}
} else {
DivParent.insertBefore(DivRow, DivParent.rows[0]);
}
}
}
function AlertRow(InR, InRowDesc, InText) {
alert("Row " + (InR + 1) + " \"" + InRowDesc + "\":\n" + InText);
}
function BuildForecastArr() {
ForecastArr = [];
var table = document.getElementById("InOutTableBody");
for (var r = 0; r < table.rows.length; r++) { // validate rows - really just verify if there's no freq, and start and end aren't the same, error
if (table.rows[r].cells[3].innerHTML === "" && // no freq specified...
table.rows[r].cells[0].innerHTML != table.rows[r].cells[1].innerHTML) { // ... and start and end dates are NOT the same
AlertRow(r, table.rows[r].cells[2].innerHTML, "must have Start and End dates the same or Freq specified.");
return false;
}
}
// get some starting points and validate
var TStartEl = document.getElementById('StartDate');
var TEndEl = document.getElementById('EndDate');
var TBalEl = document.getElementById('StartBal');
if (TStartEl.value === "") { // set Start date to today if unset
TStartEl.value = Date.today().toString('yyyy-MM-dd');
}
if (TEndEl.value === "" || TEndEl.value < TStartEl.value) { // set End date to a year from today if unset or less than Start date
TEndEl.value = Date.parse(TStartEl.value).addYears(1).toString('yyyy-MM-dd');
}
if (TBalEl.value === "") { //set balance to zero if unset
TBalEl.value = 0;
}
if (TBalEl.value != TBalEl.value.match(/[0-9.]*/)) { //set balance to zero if non-integer
TBalEl.value = 0;
}
TStart = new Date.parse(TStartEl.value);
TEnd = new Date.parse(TEndEl.value);
TBal = TBalEl.value;
for (var r = 0; r < table.rows.length; r++) { // start looping through rows!
var RowStart = new Date.parse(table.rows[r].cells[0].innerHTML);
var RowEnd = new Date.parse(table.rows[r].cells[1].innerHTML);
var RowDesc = table.rows[r].cells[2].innerHTML;
var RowFreqN = table.rows[r].cells[3].innerHTML.match(/[0-9]*/);
var RowFreqP = table.rows[r].cells[3].innerHTML.match(/[dDwWmMyY]/);
var RowDebit = table.rows[r].cells[4].innerHTML;
var RowCredit = table.rows[r].cells[5].innerHTML;
var RowColor = table.rows[r].cells[6].getElementsByClassName('cp')[0].value;
if (!isFinite(RowStart)) {
AlertRow(r, RowDesc, "Does not have a valid start date.");
return false;
}
if (RowStart > RowEnd) { // if row start is after end, set end = start
RowEnd = RowStart;
table.rows[r].cells[1].innerHTML = RowEnd.toString('yyyy-MM-dd');
}
if (RowStart > TEnd) { // if row start is after selected end, skip
continue;
}
// time to start looping through row increments
if (RowStart < TStart) { // find start of row - by row or by def
var StartReal = TStart;
} else {
var StartReal = RowStart;
}
if (RowEnd !== "" && RowEnd < TEnd) { // find end of row - by row or by def thankfully (blank & letters are < numbers)
var EndReal = RowEnd;
} else {
var EndReal = TEnd;
}
if (RowStart.valueOf() !== RowEnd.valueOf()) { // validate RowFreq[N|P] - important since we're not looping
if (typeof RowFreqN === 'undefined' || RowFreqN == "" || RowFreqN < 0) {
AlertRow(r, RowDesc, "Freq amount less than 1.\nSetting to 1.");
RowFreqN = 1;
}
if (RowFreqN % 1 !== 0) {
RowFreqN = Math.round(RowFreqN);
AlertRow(r, RowDesc, "Freq amount non-integer.\nRounded to: " + RowFreqN);
}
//console.log("RowFreqN: " + RowFreqN);
if (typeof RowFreqP === 'undefined' || RowFreqP == "") {
AlertRow(r, RowDesc, "Freq type missing.\nSetting to m (month).");
RowFreqP = "m";
}
if ("dwmy".indexOf(RowFreqP) == -1) {
AlertRow(r, RowDesc, "Freq type invalid: " + RowFreqP + "\nSetting to m (month).");
RowFreqP = "m";
}
table.rows[r].cells[3].innerHTML = RowFreqN.toString() + RowFreqP;
} else {
RowFreqP = "none"
}
ThisDate = RowStart;
do { // start looping 'x periods' through range
if (ThisDate.between(StartReal, EndReal)) {
FCLen = ForecastArr.length;
ForecastArr[FCLen] = new Array;
ForecastArr[FCLen][0] = ThisDate.toString('yyyy-MM-dd');;
ForecastArr[FCLen][1] = RowDesc;
ForecastArr[FCLen][2] = RowDebit;
ForecastArr[FCLen][3] = RowCredit;
ForecastArr[FCLen][4] = 0;
ForecastArr[FCLen][5] = RowColor;
}
var timewarp = 1 * RowFreqN;
if (timewarp == 0) timewarp = 1;
//console.log("Row:" + r + " - RowFreqN: " + RowFreqN + " - RowFreqP: " + RowFreqP + " - ThisDate: " + ThisDate.toString('yyyy-MM-dd') + " - StartReal: " + StartReal.toString('yyyy-MM-dd') + " - EndReal: " + EndReal.toString('yyyy-MM-dd') + " - Desc: " + RowDesc);
if (RowFreqP == "none") break; // no frequency means run once and we're done with row.
if (RowFreqP == "d") ThisDate.addDays(timewarp);
if (RowFreqP == "w") ThisDate.addWeeks(timewarp);
if (RowFreqP == "m") ThisDate.addMonths(timewarp);
if (RowFreqP == "y") ThisDate.addYears(timewarp);
} while (ThisDate.toString('yyyy-MM-dd') <= EndReal.toString('yyyy-MM-dd')); // end of looping 'x period' through range
}
ForecastArr.sort(function(a, b) { // sort by date and description simultaneously. lazy+fast
colsA = a[0] + a[1];
colsB = b[0] + b[1];
return colsA.toLowerCase().localeCompare(colsB.toLowerCase());
});
SaveBuildData();
return true;
}
function ClearForecastTable() {
ForeCastClear = document.getElementById("forecastTrack");
while (ForeCastClear.hasChildNodes()) { // .. clear it out first
ForeCastClear.removeChild(ForeCastClear.lastChild);
}
LowPointer = document.getElementById("lowpointer");
LowPointer.style.left = 0;
LowPointer.style.top = 0;
LowPointer.style.visibility = 'hidden';
}
function DrawForecastTable() { // ------------- DRAW THE TABLE! -------------
{ // gotta put table somewhere
StatusF = document.getElementById("forecastTrack");
ClearForecastTable(); { // add header rows to array
ForecastArr.unshift(["", "Start Balance", "", StartBal.value, "", "#BBBBBB"]);
ForecastArr.unshift(["Date", "Description", "Out", "In", "Bal", "#BBBBBB"]);
}
} { // create table/tbody, set some vars
var OutTable = document.createElement('table');
var OTBody = document.createElement('tbody');
var BalTrack = 0;
LastMonth = "start things off";
LowestRow = 0;
ForecastArr[LowestRow][6] = "";
FirstNegRow = 0
OutTable.appendChild(OTBody);
StatusF.appendChild(OutTable);
}
for (var r = 0; r < ForecastArr.length; r++) { // dump the arr. do some math along the way.
var NewRow = document.createElement('tr');
NewRow.style.backgroundColor = ForecastArr[r][5];
for (var c = 0; c < 5; c++) {
var NewCell = document.createElement('td');
var MyText = ForecastArr[r][c];
if (r > 0) { // track that balance, show me what you're working with
if (MyText !== "" && 1 < c && c < 4) {
if (c == 2) {
BalTrack = Number(BalTrack * 1 - parseFloat(ForecastArr[r][c])).toFixed(2);
}
if (c == 3) {
BalTrack = Number(BalTrack * 1 + parseFloat(ForecastArr[r][c])).toFixed(2);
}
}
ForecastArr[r][6] = BalTrack;
if (BalTrack < ForecastArr[LowestRow][6]) { // track the lowest balance
LowestRow = r * 1;
}
if (MyText !== "") {
switch (c) {
case 2:
case 3:
var MyText = Number(parseFloat(ForecastArr[r][c])).toFixed(2).toString();
break;
case 4:
var MyText = BalTrack;
break;
default:
var MyText = ForecastArr[r][c];
break;
}
}
if (BalTrack < 0) { // if negative, italicize. track first negative row too.
NewCell.style.fontStyle = "italic";
if (FirstNegRow != 0) FirstNegRow = r * 1;
}
}
NewCell.appendChild(document.createTextNode(MyText));
if (c < 2) { // first 2 columns left aligned, the rest right aligned
NewCell.className = "left";
} else {
NewCell.className = "right";
}
NewRow.appendChild(NewCell);
}
CurMonth = ForecastArr[r][0].split("-")[1];
if (CurMonth != LastMonth && r > 1) { // separate months
NewRow.className = "outbordersep";
LastMonth = CurMonth;
} else {
NewRow.className = "outborder";
}
OTBody.appendChild(NewRow);
} { // highlight lowest balance row
for (c = 0; c < 5; c++) {
OTBody.rows[LowestRow].cells[c].style.fontWeight = "bold";
}
OTBody.rows[LowestRow].className = "outborderlow";
PosX = OutTable.offsetLeft + OTBody.rows[LowestRow].offsetLeft + OTBody.rows[LowestRow].offsetWidth + 7;
PosY = OutTable.offsetTop + OTBody.rows[LowestRow].offsetTop + Math.round(OTBody.rows[LowestRow].offsetHeight / 2) - 7;
lowpointer = document.getElementById("lowpointer");
lowpointer.style.visibility = 'visible';
lowpointer.style.left = PosX + "px";
lowpointer.style.top = PosY + "px";
}
ClearButton = document.createElement('button');
ClearButton.className = "warnbutton";
ClearButton.innerHTML = "Clear Forecast";
ClearButton.onclick = ClearForecastTable;
ClearButton.style.width='160px';
ClearButton.style.height='44px';
StatusF.appendChild(ClearButton);
}
function Forecast() {
if (BuildForecastArr()) {
DrawForecastTable();
}
}
window.onload = function() {
var filesInput = document.getElementById("files");
var LoadInfo = document.getElementById("LoadInfo");
filesInput.addEventListener(
"change", function(event) {
var files = event.target.files; //FileList object
var output = document.getElementById("result");
var file = files[0];
var reader = new FileReader();
reader.addEventListener(
"load", function(event) {
var textFile = event.target;
var div = document.createElement("div");
FileIn = textFile.result;
if (typeof FileIn !== 'undefined') {
LoadTable(FileIn);
var NameFile = "Data Source: " + filesInput.value.replace(/.*\\/, "");
LoadTime = new Date().toString('yyyy-MM-dd HH:mm:ss');
LoadInfo.innerHTML = NameFile + " @ " + LoadTime;
}
}
);
reader.readAsText(file);
}
);
LoadBuildData();
}
Start Date:
End Date:
Balance:
Start
End
Name
Freq
Recurrance:
[every][period]
1y - every 1 years
2m - every 2 months
4w - every 4 weeks
14d - every 14 days
Debit
Credit
Do
← Low ←
** KNOWN BUGS **
1) when using months periods (Nm) and next calculated date exceeds days in a month, the date is thereafter modified.
example: recurrance 1m on 31st hits 30 day month, then uses 30 thereafter, and again when it hits February, using 28 thereafter.
This does not affect year (y), week (w), or days (d) periods.
This is an artifact of work being performed via "add period to last date" - when Feb 31st is encountered 'addMonths' function in date.min.js returns sane date.
A fix is not immediately apparent and affects a small slice of data moving certain items up to 3 days earlier.
2) lowest value indicator does not currently function
Report bugs to rake74@hotmail.com