[\\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==3){/* 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==4){/* 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;}aCell.innerHTML=LowerVal; OkCell(aCell); return true;}if (aCell.cellIndex >=5){/* 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 colorpickerChanged(aColorpicker){aRow=aColorpicker.closest("tr"); CellBG=aColorpicker.value; for (var c=1; c < aRow.cells.length; c++){aRow.cells[c].bgColor=CellBG;}}function MakeCell(aRow, numCell, innerText, Editable){Editable=Editable || "true"; innerText=innerText || ""; var aCell=aRow.insertCell(numCell); if (numCell==0){/* grabber to move row */ aCell.innerHTML=innerText; aCell.className="sorter"; aCell.setAttribute('align', 'center'); aCell.setAttribute('valign', 'middle'); aCell.setAttribute('bgcolor', 'A9A9A9');}else if (numCell <=6){/* user input fields */ aCell.innerHTML=innerText; aCell.setAttribute('contenteditable', Editable); aCell.addEventListener("blur", ValidateCell); if (numCell==3) aCell.className="left"; if (numCell >=5) aCell.className="right";}else if (numCell==7){/* color picker */ /* container div for overlaid pen + tool divs */ var containerdiv=document.createElement("div"); containerdiv.setAttribute("class", "colorpickerContainer"); /* div for pen character */ var pendiv=document.createElement("div"); pendiv.setAttribute("class", "colorpickerPen"); pendiv.innerHTML="🖊"; /* div for colorpicker input */ var tooldiv=document.createElement("div"); tooldiv.setAttribute("class", "colorpickerTool"); /* colorpicker tool */ var colorpicker=document.createElement("input"); colorpicker.setAttribute("type", "color"); colorpicker.setAttribute("class", "colorpicker"); if (innerText !==""){colorpicker.setAttribute("value", innerText);}else{colorpicker.setAttribute("value", "#F5F5F5");}colorpicker.setAttribute("oninput", "colorpickerChanged(this)"); tooldiv.appendChild(colorpicker); containerdiv.appendChild(pendiv); containerdiv.appendChild(tooldiv); aCell.appendChild(containerdiv); /* all so that we can add the onchange element and have it register as an event */ colorpickerChanged(colorpicker);}else if (numCell==8){/* delete row button */ aCell.className="x"; aCell.innerHTML="
x
";}}function AddRow(LineArr){LineArr=LineArr || ["", "", "", "", "", "", "", "", ""]; LineArr.unshift("☰"); /* insert grabber to move */ var IOTable=document.getElementById("InOutTableBody"); var NewRow=IOTable.insertRow(IOTable.rows.length); for (var x=0; x < 9; x++){MakeCell(NewRow, x, LineArr[x]);}}function SaveTable(){BuildDataToSave(); 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"; 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);}}}}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 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[4].innerHTML==="" && /* no freq specified... */ table.rows[r].cells[1].innerHTML !=table.rows[r].cells[2].innerHTML){/* ... and start and end dates are NOT the same */ AlertRow(r, table.rows[r].cells[3].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[1].innerHTML); var RowEnd=new Date.parse(table.rows[r].cells[2].innerHTML); var RowDesc=table.rows[r].cells[3].innerHTML; var RowFreqN=table.rows[r].cells[4].innerHTML.match(/[0-9]*/); var RowFreqP=table.rows[r].cells[4].innerHTML.match(/[dDwWmMyY]/); var RowDebit=table.rows[r].cells[5].innerHTML; var RowCredit=table.rows[r].cells[6].innerHTML; var RowColor=table.rows[r].cells[7].getElementsByClassName('colorpicker')[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[2].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);}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[4].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; LowestBal=StartBal.value; 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 (r > 1){if (parseFloat(BalTrack) < LowestBal){/* track the lowest balance */ LowestRow=r; LowestBal=BalTrack;}}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 weeks14d - every 14 days
Debit
Credit
Do
← Low ←
** KNOWN BUGS **
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.