diff --git a/js/Utl.js b/js/Utl.js new file mode 100644 index 0000000..7cc4a6e --- /dev/null +++ b/js/Utl.js @@ -0,0 +1,1348 @@ +/* +Copyright (c) 2023-forever Douglas Malnati. All rights reserved. + +See the /faq/tos page for details. + +(If this generated header is stamped on a file which is a 3rd party file or under a different license or copyright, then ignore this copyright statement and use that file's terms.) +*/ + +const INTL_NUMBER_FORMAT_EN_US = new Intl.NumberFormat("en-US"); + +export class StrAccumulator +{ + constructor() + { + this.str = ``; + this.indent = 0; + this.sep = ""; + } + + A(appendStr) + { + this.str = `${this.str}${this.sep}${" ".repeat(this.indent)}${appendStr}`; + this.sep = "\n"; + } + + IncrIndent() + { + this.indent += 4; + } + + DecrIndent() + { + this.indent -= 4; + } + + Get() + { + return this.str; + } +} + +export function Commas(num) +{ + if (num == null) + { + return num; + } + + if (typeof num == "number") + { + if (!Number.isFinite(num)) + { + return `${num}`; + } + + let wholePart = Math.trunc(num); + let wholeStr = INTL_NUMBER_FORMAT_EN_US.format(wholePart); + + if (Number.isInteger(num)) + { + return wholeStr; + } + + let numStr = `${num}`; + let dotIdx = numStr.indexOf("."); + if (dotIdx == -1) + { + return wholeStr; + } + + return wholeStr + numStr.substring(dotIdx); + } + + if (typeof num == "string") + { + let str = num.trim(); + if (str == "") + { + return str; + } + + let sign = ""; + if (str[0] == "-" || str[0] == "+") + { + sign = str[0]; + str = str.substring(1); + } + + let dotIdx = str.indexOf("."); + let wholePart = dotIdx == -1 ? str : str.substring(0, dotIdx); + let fracPart = dotIdx == -1 ? "" : str.substring(dotIdx); + + if (!/^\d+$/.test(wholePart)) + { + return num; + } + + let wholeStr = INTL_NUMBER_FORMAT_EN_US.format(Number(wholePart)); + return sign + wholeStr + fracPart; + } + + return `${num}`; +} + +// take in any css color string (hex, rgb, name, etc) +// return array [r, g, b] +export function GetRgbFromColor(color) +{ + let d = document.createElement("div"); + + d.style.color = color; + + + document.body.appendChild(d); + // comes out as "rgb(r, g, b)" + let rgbStrColor = window.getComputedStyle(d).color; + document.body.removeChild(d); + + // extract r, g, b + let rgbArr = rgbStrColor.substring(4, rgbStrColor.length-1).replace(/ /g, '').split(','); + + rgbArr[0] = parseInt(rgbArr[0]); + rgbArr[1] = parseInt(rgbArr[1]); + rgbArr[2] = parseInt(rgbArr[2]); + + return rgbArr; +} + + + +// num1 can be higher/lower/equal to num2 +// pct is a decimal value, so 50 means 50% +// return a number that is pct between num1 and num2 +export function PctBetween(pct, num1, num2) +{ + if (pct < 0) { pct = 0; } + if (pct > 100) { pct = 100; } + + let min = Math.min(num1, num2); + let max = Math.max(num1, num2); + + let diff = max - min; + + if (diff < 0) + { + diff = -diff; + } + + let retVal = min + ((pct / 100) * diff); + + return retVal; +} + + +// returns a string of "rgb(r, g, b)" +export function ColorPctBetween(pct, colorStart, colorEnd, toInt = false) +{ + let rgbArrStart = GetRgbFromColor(colorStart); + let rgbArrEnd = GetRgbFromColor(colorEnd); + + let r = PctBetween(pct, rgbArrStart[0], rgbArrEnd[0]); + let g = PctBetween(pct, rgbArrStart[1], rgbArrEnd[1]); + let b = PctBetween(pct, rgbArrStart[2], rgbArrEnd[2]); + + if (toInt) + { + r = Math.round(r); + g = Math.round(g); + b = Math.round(b); + } + + let retVal = `rgb(${r}, ${g}, ${b})`; + + return retVal; +} + + +export function Now() +{ + return Date.now(); +} + +export function DateYYYY() +{ + return `${(new Date()).getFullYear()}`; +} + +export function DateMM() +{ + return String((new Date()).getMonth() + 1).padStart(2, '0'); +} + +export function DateDD() +{ + return String((new Date()).getDate()).padStart(2, '0'); +} + +export function TimeHH() +{ + return String((new Date()).getHours()).padStart(2, '0'); +} + +export function TimeMM() +{ + return String((new Date()).getMinutes()).padStart(2, '0'); +} + +export function TimeSS() +{ + return String((new Date()).getSeconds()).padStart(2, '0'); +} + +export function ValOrDefaultFn(str, fn) +{ + let retVal; + + if (str) + { + str = String(str).trim(); + + if (str == "") + { + retVal = fn(); + } + else + { + retVal = str; + } + } + else + { + retVal = fn(); + } + + return retVal; +} + +export function ValOrDefault(str, strDefault) +{ + let retVal; + + if (str) + { + str = String(str).trim(); + + if (str == "") + { + retVal = strDefault; + } + else + { + retVal = str; + } + } + else + { + retVal = strDefault; + } + + return retVal; +} + +export function ParseDateTimeToIsoString(timeStr) +{ + timeStr = timeStr.trim(); + let timePartArr = timeStr.trim().split(" "); + + let YYYY, MM, DD; + let hh, mm, ss; + + if (timePartArr.length >= 1) + { + let date = timePartArr[0]; + + let datePartList = date.split("-"); + + if (datePartList.length >= 1) { YYYY = datePartList[0]; } + if (datePartList.length >= 2) { MM = datePartList[1].padStart(2, '0'); } else { MM = "01"; } + if (datePartList.length >= 3) { DD = datePartList[2].padStart(2, '0'); } else { DD = "01"; } + } + + if (timePartArr.length >= 2) + { + let time = timePartArr[1]; + + let timePartList = time.split(":"); + + if (timePartList.length >= 1) { hh = timePartList[0].padStart(2, '0'); } else { hh = "00"; } + if (timePartList.length >= 2) { mm = timePartList[1].padStart(2, '0'); } else { mm = "00"; } + if (timePartList.length >= 3) { ss = timePartList[2].padStart(2, '0'); } else { ss = "00"; } + } + + YYYY = ValOrDefault(YYYY, DateYYYY()); + MM = ValOrDefault(MM, "01"); + DD = ValOrDefault(DD, "00"); + hh = ValOrDefault(hh, "00"); + mm = ValOrDefault(mm, "00"); + ss = ValOrDefault(ss, "00"); + + let timeStrIso = `${YYYY}-${MM}-${DD}T${hh}:${mm}:${ss}`; + + return timeStrIso; +} + +export function ConvertUtcToLocal(timeStr) +{ + let isoStr = ParseDateTimeToIsoString(timeStr); + + isoStr += "Z"; + + let ms = (new Date(isoStr)).getTime(); + + let localTimeStr = MakeDateTimeFromMs(ms); + + return localTimeStr; +} + +export function ConvertLocalToUtc(timeStr) +{ + // we have a local time, what is that in UTC? + // it is as different to us as we are from it + + // first, get the local time in ms + let isoStr = ParseDateTimeToIsoString(timeStr); + let msLocal = (new Date(isoStr)).getTime(); + + // next, get that same local time in ms, but pretend we're in UTC + let isoStrUtc = isoStr + "Z"; + let msUtcTmp = (new Date(isoStrUtc)).getTime(); + + // look at the difference + let msDiff = msUtcTmp - msLocal; + + // calculate + let msUtc = msLocal - msDiff; + + // convert to str + let utcTimeStr = MakeDateTimeFromMs(msUtc); + + return utcTimeStr; +} + +export function ParseTimeToMs(timeStr) +{ + let timeStrIso = ParseDateTimeToIsoString(timeStr); + + let ms = Date.parse(timeStrIso); + + return ms; +} + +export function MsDiff(time1, time2) +{ + let msTime1 = ParseTimeToMs(time1); + let msTime2 = ParseTimeToMs(time2); + + return msTime1 - msTime2; +} + +export function MsUntil(timeStr) +{ + let msNow = Now(); + let msThen = ParseTimeToMs(timeStr); + let msUntil = msThen - msNow; + + if (timeStr == "") + { + msUntil = 0; + } + + return msUntil; +} + +export function MsUntilDate(dateStr) +{ + let msNow = ParseTimeToMs(MakeDateFromMs(Now())); + let msThen = ParseTimeToMs(dateStr); + let msUntil = msThen - msNow; + + if (dateStr == "") + { + msUntil = 0; + } + + return msUntil; +} + +export function DateTimeValid(timeStr) +{ + return isNaN(ParseTimeToMs(timeStr)) == false; +} + +export function MakeDateTimeFromDateTime(timeStr) +{ + let timeStrIso = ParseDateTimeToIsoString(timeStr); + + // drop the T + let partList = timeStrIso.split("T"); + + let timeStrIsoNoT = `${partList[0]} ${partList[1]}`; + + return timeStrIsoNoT; +} + +export function MakeDateFrom(timeStr) +{ + let dateTime = MakeDateTimeFromDateTime(timeStr); + + return dateTime.split(" ")[0]; +} + +export function MakeDateTimeFromMs(ms) +{ + let dt = new Date(ms); + + let YYYY = dt.getFullYear(); + let MM = String((dt.getMonth() + 1)).padStart(2, '0') + let DD = String(dt.getDate()).padStart(2, '0') + let hh = String(dt.getHours()).padStart(2, '0') + let mm = String(dt.getMinutes()).padStart(2, '0') + let ss = String(dt.getSeconds()).padStart(2, '0') + + let dateTime = `${YYYY}-${MM}-${DD} ${hh}:${mm}:${ss}`; + + return dateTime; +} + +export function MakeDateTimeNow() +{ + return MakeDateTimeFromMs(Now()); +} + +export function MakeDateFromMs(ms) +{ + let dateTime = MakeDateTimeFromMs(ms); + + return dateTime.split(" ")[0]; +} + +// only return the non-zero elements +// eg 3 days, 0 hours, 5 mins = 3 days, 0 hours, 5 mins +// eg 0 days, 0 hours, 5 mins = 5 mins +// eg 0 days, 0 hours, 0 mins = 0 mins +export function DurationStrTrim(str) +{ + let partList = str.split(","); + let partListNew = []; + + let keepRemaining = false; + for (let i = 0; i < partList.length; ++i) + { + let part = partList[i].trim(); + + if (keepRemaining) + { + partListNew.push(partList[i]); + } + else if (part.charAt(0) != "0") + { + partListNew.push(partList[i]); + keepRemaining = true; + } + else + { + // do nothing, drop + } + } + + if (partListNew.length == 0) + { + partListNew.push(partList[partList.length - 1]); + } + + return partListNew.join(",").trim(); +} + +export function MsToDurationStrDaysHoursMinutes(ms) +{ + let full = MsToDurationStrDaysHoursMinutesSeconds(ms); + + return full.split(",").slice(0, 3).join(",").trim(); +} + +export function MsToDurationStrMinutes(ms) +{ + let full = MsToDurationStrDaysHoursMinutesSeconds(ms); + + return full.split(",").slice(2, 3).join(",").trim(); +} + +export function MsToDurationStrDaysHoursMinutesSeconds(ms) +{ + let val = ms; + + // ignore ms + val = Math.floor(val / 1000); + + // how many days? + let days = Math.floor(val / (24 * 60 * 60)); + val -= days * (24 * 60 * 60); + + // how many hours? + let hours = Math.floor(val / (60 * 60)); + val -= hours * (60 * 60); + + // how many minutes? + let mins = Math.floor(val / 60); + val -= mins * 60; + + // how many seconds? + let secs = val; + + let duration = ""; + duration += `${days}` + (days == 1 ? " day" : " days"); + duration += ", "; + duration += `${hours}` + (hours == 1 ? " hour" : " hours"); + duration += ", "; + duration += `${mins}` + (mins == 1 ? " min" : " mins"); + duration += ", "; + duration += `${secs}` + (secs == 1 ? " sec" : " secs"); + + return duration; +} + + +export function Rotate(arr, count) +{ + let retVal = [... arr]; + + if (count > 0) + { + while (count) + { + retVal.unshift(retVal.pop()); + + --count; + } + } + else if (count < 0) + { + count = -count; + + while (count) + { + retVal.push(retVal.shift()); + + --count; + } + } + + return retVal; +} + +export function ListIntersectionUnique(leftList, rightList) +{ + let retVal = []; + + if (leftList.length == 0) + { + retVal = rightList; + } + else if (rightList.length == 0) + { + retVal = leftList; + } + else + { + let m = new Map(); + + for (const left of leftList) + { + m.set(left, 1); + } + + for (const right of rightList) + { + if (m.has(right)) + { + m.set(right, 2); + } + } + + for (let [key, val] of m) + { + if (val == 2) + { + retVal.push(key); + } + } + } + + + retVal.sort(); + + return retVal; +} + + +export function GetSearchParam(paramName, defaultValue) +{ + let retVal = defaultValue; + const params = new URLSearchParams(window.location.search); + + if (params.has(paramName)) + { + let val = params.get(paramName); + + if (val != "null" && val != undefined && val != "undefined") + { + retVal = val.trim(); + } + } + + return retVal; +} + +export function SetDomValBySearchParam(dom, paramName, defaultValue) +{ + const params = new URLSearchParams(window.location.search); + + dom.value = GetSearchParam(paramName, defaultValue); +} + +export function SetDomCheckedBySearchParam(dom, paramName) +{ + const params = new URLSearchParams(window.location.search); + + dom.checked = params.get(paramName) != "0"; +} + + +export function StrToCssClassName(col) +{ + return col.replace(/[.\s]/g, '_'); +}; + +export function MakeTable(dataTable, synthesizeRowCountColumn) +{ + // build table + let table = document.createElement("table"); + + let rowHeader = dataTable[0]; + let headerClassDataList = rowHeader.map(colVal => { + let colClass = ""; + let hdrClass = ""; + let dataClass = ""; + + try + { + colClass = StrToCssClassName(`${colVal}_col`); + hdrClass = StrToCssClassName(`${colVal}_hdr`); + dataClass = StrToCssClassName(`${colVal}_data`); + } + catch (e) + { + } + + return { + colClass, + hdrClass, + dataClass, + }; + }); + + let EscapeHtml = (val) => { + return String(val) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); + }; + + let MakeClassAttr = (classList) => { + let filtered = classList.filter(str => str != ""); + if (filtered.length == 0) + { + return ""; + } + + return ` class="${filtered.join(" ")}"`; + }; + + let SetTableCellContent = (dom, val) => { + if (val instanceof Element) + { + dom.appendChild(val); + } + else if (typeof val == "string" && val.indexOf("<") != -1) + { + dom.innerHTML = val; + } + else + { + dom.textContent = val ?? ""; + } + }; + + // build column group which allows styling to affect the entire column if desired + // https://stackoverflow.com/questions/27234480/can-i-color-table-columns-using-css-without-coloring-individual-cells + let colgroup = document.createElement("colgroup"); + if (synthesizeRowCountColumn) + { + let col = document.createElement("col"); + col.classList.add(StrToCssClassName("row_col")); + colgroup.appendChild(col); + } + + for (let idx = 0; idx < rowHeader.length; ++idx) + { + let col = document.createElement("col"); + let colClass = headerClassDataList[idx].colClass; + if (colClass != "") + { + col.classList.add(colClass); + } + colgroup.appendChild(col); + } + + table.appendChild(colgroup); + + + // build header + let thead = document.createElement("thead"); + let trHeader = document.createElement("tr"); + trHeader.classList.add(StrToCssClassName("headerRow")); + + if (synthesizeRowCountColumn) + { + let thRow = document.createElement("th"); + thRow.innerHTML = "row"; + thRow.classList.add(StrToCssClassName("row_col")); + thRow.classList.add(StrToCssClassName(`row_hdr`)); + trHeader.appendChild(thRow); + } + + for (let idx = 0; idx < rowHeader.length; ++idx) + { + const colVal = rowHeader[idx]; + let th = document.createElement("th"); + th.textContent = colVal; + + let colClass = headerClassDataList[idx].colClass; + let hdrClass = headerClassDataList[idx].hdrClass; + if (colClass != "") + { + th.classList.add(colClass); + } + if (hdrClass != "") + { + th.classList.add(hdrClass); + } + + trHeader.appendChild(th); + } + + thead.appendChild(trHeader); + table.appendChild(thead); + + + // build body + let tbody = document.createElement("tbody"); + let useFastStringPath = true; + for (let i = 1; i < dataTable.length && useFastStringPath; ++i) + { + for (const colVal of dataTable[i]) + { + if (colVal instanceof Element) + { + useFastStringPath = false; + break; + } + } + } + + if (useFastStringPath) + { + let html = ""; + + for (let i = 1; i < dataTable.length; ++i) + { + html += "