diff --git a/benchexec/tablegenerator/react-table/src/components/StatisticsTable.js b/benchexec/tablegenerator/react-table/src/components/StatisticsTable.js index 2e86c524a..156c3f902 100644 --- a/benchexec/tablegenerator/react-table/src/components/StatisticsTable.js +++ b/benchexec/tablegenerator/react-table/src/components/StatisticsTable.js @@ -8,23 +8,23 @@ import React, { useState, useMemo, useEffect } from "react"; import { - useTable, - useFilters, - useResizeColumns, - useFlexLayout, + useTable, + useFilters, + useResizeColumns, + useFlexLayout, } from "react-table"; import { useSticky } from "react-table-sticky"; import { - createRunSetColumns, - SelectColumnsButton, - StandardColumnHeader, + createRunSetColumns, + SelectColumnsButton, + StandardColumnHeader, } from "./TableComponents"; import { computeStats, statisticsRows } from "../utils/stats.js"; import { - determineColumnWidth, - isNumericColumn, - isNil, - getHiddenColIds, + determineColumnWidth, + isNumericColumn, + isNil, + getHiddenColIds, } from "../utils/utils"; const isTestEnv = process.env.NODE_ENV === "test"; @@ -47,8 +47,8 @@ const StatisticsTable = ({ stats: defaultStats, filtered = false, }) => { - // We want to skip stat calculation in a test environment if not - // specifically wanted (signaled by a passed onStatsReady callback function) + // We want to skip stat calculation in a test environment if not + // specifically wanted (signaled by a passed onStatsReady callback function) const skipStats = isTestEnv && !onStatsReady; // When filtered, initialize with empty statistics until computed statistics @@ -58,247 +58,256 @@ const StatisticsTable = ({ // we want to trigger a re-calculation of our stats whenever data changes. useEffect(() => { - const updateStats = async () => { - if (filtered) { - const newStats = await computeStats({ - tools, - tableData, - stats: defaultStats, - }); - setStats(newStats); - } else { - setStats(defaultStats); - } - if (onStatsReady) { - onStatsReady(); - } + const updateStats = async() => { + if (filtered) { + const newStats = await computeStats({ + tools, + tableData, + stats: defaultStats, + }); + setStats(newStats); + } else { + setStats(defaultStats); + } + if (onStatsReady) { + onStatsReady(); + } }; if (!skipStats) { - updateStats(); // necessary such that hook is not async + updateStats(); // necessary such that hook is not async } - }, [tools, tableData, onStatsReady, skipStats, defaultStats, filtered]); +}, [tools, tableData, onStatsReady, skipStats, defaultStats, filtered]); - const renderTableHeaders = (headerGroups) => ( -
- {headerGroups.map((headerGroup) => ( -
- {headerGroup.headers.map((header) => ( -
( < + div className = "table-header" > { + headerGroups.map((headerGroup) => ( < + div className = "tr headergroup" {...headerGroup.getHeaderGroupProps() } > { + headerGroup.headers.map((header) => ( < + div {...header.getHeaderProps({ + className: `th header ${header.headers ? "outer " : ""}${ header.className || "" }`, - })} - > - {header.render("Header")} + }) + } > { header.render("Header") } - {(!header.className || - !header.className.includes("separator")) && ( -
- )} -
- ))} + { + (!header.className || + !header.className.includes("separator")) && ( < + div {...header.getResizerProps() } + className = { `resizer ${header.isResizing ? "isResizing" : ""}` } + /> + ) + } +
+ )) + } +
+ )) + }
- ))} - - ); - - const renderTableData = (rows) => ( -
- {rows.map((row) => { - prepareRow(row); - return ( -
- {row.cells.map((cell) => ( -
- {cell.render("Cell")} -
- ))} -
- ); - })} -
- ); + ); - const renderTable = (headerGroups, rows) => { - if (filtered && stats.length === 0) { - return ( -

- Please wait while the statistics are being calculated. -

- ); - } - return ( -
-
-
-
- {renderTableHeaders(headerGroups)} - {renderTableData(rows)} -
-
+ const renderTableData = (rows) => ( < + div {...getTableBodyProps() } + className = "table-body body" > { + rows.map((row) => { + prepareRow(row); + return ( < + div {...row.getRowProps() } + className = "tr" > { + row.cells.map((cell) => ( < + div {...cell.getCellProps({ + className: "td " + (cell.column.className || ""), + }) + } > { cell.render("Cell") } +
+ )) + } +
+ ); + }) + } - ); - }; - const columns = useMemo(() => { - const createColumnBuilder = - ({ switchToQuantile, hiddenCols }) => - (runSetIdx, column, columnIdx) => ({ - id: `${runSetIdx}_${column.display_title}_${columnIdx}`, - Header: ( - switchToQuantile(column)} - /> - ), - hidden: - hiddenCols[runSetIdx].includes(column.colIdx) || - !(isNumericColumn(column) || column.type === "status"), - width: determineColumnWidth( - column, - null, - column.type === "status" ? 6 : null, - ), - minWidth: 30, - accessor: (row) => row.content[runSetIdx][columnIdx], - Cell: (cell) => { - let valueToRender = cell.value?.sum; - // We handle status differently as the main aggregation (denoted "sum") - // is of type "count" for this column type. - // This means that the default value if no data is available is 0 - if (column.type === "status") { - if (cell.value === undefined) { - // No data is available, default to 0 - valueToRender = 0; - } else if (cell.value === null) { - // We receive a null value directly from the stats object of the dataset. - // Will be rendered as "-" - // This edge case only applies to the local summary as it contains static values - // that we can not calculate and therefore directly take them from the stats object. + const renderTable = (headerGroups, rows) => { + if (filtered && stats.length === 0) { + return ( +

+ Please wait + while the statistics are being calculated. +

+ ); + } + return ( +
+
+
+
{ renderTableHeaders(headerGroups) } { renderTableData(rows) } +
+
+
+
+ ); + }; - valueToRender = null; - } else { - valueToRender = Number.isInteger(Number(cell.value.sum)) - ? Number(cell.value.sum) - : cell.value.sum; - } - } - return !isNil(valueToRender) ? ( -
- ) : ( -
-
- ); - }, - }); + // Stutus Categories + const statusCategories = ["unknown", "TIMEOUT", "OUT OF MEMORY"]; //Add more statuses + const columns = useMemo(() => { + const createColumnBuilder = + ({ switchToQuantile, hiddenCols }) => + (runSetIdx, column, columnIdx) => ({ + id: `${runSetIdx}_${column.display_title}_${columnIdx}`, + Header: ( < + StandardColumnHeader column = { column } + className = "header-data clickable" + title = "Show Quantile Plot of this column" + onClick = { + (e) => switchToQuantile(column) + } + /> + ), + hidden: hiddenCols[runSetIdx].includes(column.colIdx) || + !(isNumericColumn(column) || column.type === "status"), + width: determineColumnWidth( + column, + null, + column.type === "status" ? 6 : null, + ), + minWidth: 30, + accessor: (row) => row.content[runSetIdx][columnIdx], + Cell: (cell) => { + let valueToRender = cell.value ? .sum ; + // We handle status differently as the main aggregation (denoted "sum") + // is of type "count" for this column type. + // This means that the default value if no data is available is 0 + if (column.type === "status") { + if (cell.value === undefined) { + // No data is available, default to 0 + valueToRender = 0; + } else if (cell.value === null) { + // We receive a null value directly from the stats object of the dataset. + // Will be rendered as "-" + // This edge case only applies to the local summary as it contains static values + // that we can not calculate and therefore directly take them from the stats object. - const createRowTitleColumn = () => ({ - Header: () => ( -
- -
- ), - id: "row-title", - sticky: isTitleColSticky ? "left" : "", - width: titleColWidth, - minWidth: 100, - columns: [ - { - id: "summary", - width: titleColWidth, - minWidth: 100, - Header: , - Cell: (cell) => ( -
- ), - }, - ], - }); + valueToRender = null; + } else { + valueToRender = Number.isInteger(Number(cell.value.sum)) ? + Number(cell.value.sum) : + cell.value.sum; + } + } + return !isNil(valueToRender) ? ( < + div dangerouslySetInnerHTML = { + { + __html: valueToRender, + } + } + className = "cell" + title = { + column.type !== "status" ? renderTooltip(cell.value) : undefined + } > +
+ ) : ( +
-
+ ); + }, + }); + + const createRowTitleColumn = () => ({ + Header: () => ( +
+ +
+ ), + id: "row-title", + sticky: isTitleColSticky ? "left" : "", + width: titleColWidth, + minWidth: 100, + columns: [{ + id: "summary", + width: titleColWidth, + minWidth: 100, + Header: < SelectColumnsButton handler = { selectColumn } + />, + Cell: (cell) => ( < + div dangerouslySetInnerHTML = { + { + __html: + (cell.row.original.title || + " ".repeat( + 4 * statisticsRows[cell.row.original.id].indent, + ) + statisticsRows[cell.row.original.id].title) + + (filtered ? " of selected rows" : ""), + } + } + title = { + cell.row.original.description || + statisticsRows[cell.row.original.id].description + } + className = "row-title" / + > + ), + }, ], + }); - const statColumns = tools - .map((runSet, runSetIdx) => - createRunSetColumns( - runSet, - runSetIdx, - createColumnBuilder({ switchToQuantile, hiddenCols }), - ), - ) - .flat(); + const statusColumns = statusCategories.map((status) => ({ + id: `status_${status}`, + Header: status.toUpperCase(), + accessor: (row) => row.satusCounts ? .[status] || 0, + cell: (cell) => < div > { cell.value } + })); + const statColumns = tools + .map((runSet, runSetIdx) => + createRunSetColumns( + runSet, + runSetIdx, + createColumnBuilder({ switchToQuantile, hiddenCols }), + ), + ) + .flat(); - return [createRowTitleColumn()].concat(statColumns); - }, [ - filtered, - isTitleColSticky, - switchToQuantile, - hiddenCols, - selectColumn, - tools, - ]); + return [createRowTitleColumn()].concat(statColumns, statColumns); + }, [ + filtered, + isTitleColSticky, + switchToQuantile, + hiddenCols, + selectColumn, + tools, + ]); - const data = useMemo(() => stats, [stats]); + const data = useMemo(() => stats, [stats]); - const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = - useTable( - { - columns, - data, - initialState: { - hiddenColumns: getHiddenColIds(columns), + const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = + useTable({ + columns, + data, + initialState: { + hiddenColumns: getHiddenColIds(columns), + }, }, - }, - useFilters, - useResizeColumns, - useFlexLayout, - useSticky, + useFilters, + useResizeColumns, + useFlexLayout, + useSticky, ); - return ( -
-

Statistics

- {renderTable(headerGroups, rows)} -
- ); + return ( +
+

Statistics

{ renderTable(headerGroups, rows) } +
+ ); }; -export default StatisticsTable; +export default StatisticsTable; \ No newline at end of file diff --git a/benchexec/tablegenerator/react-table/src/utils/stats.js b/benchexec/tablegenerator/react-table/src/utils/stats.js index e6bf407cd..6ab72ad95 100644 --- a/benchexec/tablegenerator/react-table/src/utils/stats.js +++ b/benchexec/tablegenerator/react-table/src/utils/stats.js @@ -11,55 +11,77 @@ import { enqueue } from "../workers/workerDirector"; const keysToIgnore = ["meta"]; export const statisticsRows = { - total: { title: "all results" }, - correct: { - indent: 1, - title: "correct results", - description: - "(property holds + result is true) OR (property does not hold + result is false)", - }, - correct_true: { - indent: 2, - title: "correct true", - description: "property holds + result is true", - }, - correct_false: { - indent: 2, - title: "correct false", - description: "property does not hold + result is false", - }, - correct_unconfirmed: { - indent: 1, - title: "correct-unconfirmed results", - description: - "(property holds + result is true) OR (property does not hold + result is false), but unconfirmed", - }, - correct_unconfirmed_true: { - indent: 2, - title: "correct-unconfirmed true", - description: "property holds + result is true, but unconfirmed", - }, - correct_unconfirmed_false: { - indent: 2, - title: "correct-unconfirmed false", - description: "property does not hold + result is false, but unconfirmed", - }, - wrong: { - indent: 1, - title: "incorrect results", - description: - "(property holds + result is false) OR (property does not hold + result is true)", - }, - wrong_true: { - indent: 2, - title: "incorrect true", - description: "property does not hold + result is true", - }, - wrong_false: { - indent: 2, - title: "incorrect false", - description: "property holds + result is false", - }, + total: { title: "all results" }, + correct: { + indent: 1, + title: "correct results", + description: "(property holds + result is true) OR (property does not hold + result is false)", + }, + correct_true: { + indent: 2, + title: "correct true", + description: "property holds + result is true", + }, + correct_false: { + indent: 2, + title: "correct false", + description: "property does not hold + result is false", + }, + correct_unconfirmed: { + indent: 1, + title: "correct-unconfirmed results", + description: "(property holds + result is true) OR (property does not hold + result is false), but unconfirmed", + }, + correct_unconfirmed_true: { + indent: 2, + title: "correct-unconfirmed true", + description: "property holds + result is true, but unconfirmed", + }, + correct_unconfirmed_false: { + indent: 2, + title: "correct-unconfirmed false", + description: "property does not hold + result is false, but unconfirmed", + }, + wrong: { + indent: 1, + title: "incorrect results", + description: "(property holds + result is false) OR (property does not hold + result is true)", + }, + wrong_true: { + indent: 2, + title: "incorrect true", + description: "property does not hold + result is true", + }, + wrong_false: { + indent: 2, + title: "incorrect false", + description: "property holds + result is false", + }, + unknown: { + indent: 1, + title: "unknown results", + description: "Result with unknown status", + }, + unknown_true: { + indent: 2, + title: "unknown true", + description: "Result with unknown status", + }, + unknown_false: { + indent: 2, + title: "unknown false", + description: "Result with unknown status", + }, + timeout: { + indent: 1, + title: "TIMEOUT results", + description: "Tasks that timed out", + }, + out_of_memory: { + indent: 1, + title: "OUT OF MEMORY results", + description: "Tasks that run out of memory", + }, }; /** @@ -67,7 +89,7 @@ export const statisticsRows = { * compute values (e.g., local summary, score). */ export const filterComputableStatistics = (stats) => - stats.filter((row) => statisticsRows[row.id]); + stats.filter((row) => statisticsRows[row.id]); /** * This method gets called on the initial render or whenever there is a @@ -78,57 +100,57 @@ export const filterComputableStatistics = (stats) => * necessary transformation to bring the calculation results into the * required format. */ -export const computeStats = async ({ tools, tableData, stats }) => { - const formatter = buildFormatter(tools); - let res = await processData({ tools, tableData, formatter }); - - const availableStats = stats - .map((row) => row.id) - .filter((id) => statisticsRows[id]); - const cleaned = cleanupStats(res, formatter, availableStats); - - // fill up stat array to match column mapping - - // The result of our stat calculation only contains relevant columns. - // The stat table however requires a strict ordering of columns that also - // includes columns that are not even rendered. - // - // In order to ensure a consistent layout we iterate through all columns - // of the runset and append dummy objects until we reach a column that we - // have calculated data for - res = cleaned.map((tool, toolIdx) => { - const out = []; - const toolColumns = tools[toolIdx].columns; - let pointer = 0; - let curr = toolColumns[pointer]; - - for (const col of tool) { - const { title } = col; - while (pointer < toolColumns.length && title !== curr.title) { - // irrelevant column - out.push({}); - pointer++; - curr = toolColumns[pointer]; - } - if (pointer >= toolColumns.length) { - break; - } - // relevant column - out.push(col); - pointer++; - curr = toolColumns[pointer]; - } +export const computeStats = async({ tools, tableData, stats }) => { + const formatter = buildFormatter(tools); + let res = await processData({ tools, tableData, formatter }); + + const availableStats = stats + .map((row) => row.id) + .filter((id) => statisticsRows[id]); + const cleaned = cleanupStats(res, formatter, availableStats); + + // fill up stat array to match column mapping + + // The result of our stat calculation only contains relevant columns. + // The stat table however requires a strict ordering of columns that also + // includes columns that are not even rendered. + + // In order to ensure a consistent layout we iterate through all columns + // of the runset and append dummy objects until we reach a column that we + // have calculated data for + res = cleaned.map((tool, toolIdx) => { + const out = []; + const toolColumns = tools[toolIdx].columns; + let pointer = 0; + let curr = toolColumns[pointer]; + + for (const col of tool) { + const { title } = col; + while (pointer < toolColumns.length && title !== curr.title) { + // irrelevant column + out.push({}); + pointer++; + curr = toolColumns[pointer]; + } + if (pointer >= toolColumns.length) { + break; + } + // relevant column + out.push(col); + pointer++; + curr = toolColumns[pointer]; + } - return out; - }); + return out; + }); - // Put new statistics in same "shape" as old ones. - return filterComputableStatistics(stats).map((row) => { - const content = row.content.map((tool, toolIdx) => { - return res[toolIdx].map((col) => col[row.id]); + // Put new statistics in same "shape" as old ones. + return filterComputableStatistics(stats).map((row) => { + const content = row.content.map((tool, toolIdx) => { + return res[toolIdx].map((col) => col[row.id]); + }); + return {...row, content }; }); - return { ...row, content }; - }); }; /** @@ -138,71 +160,71 @@ export const computeStats = async ({ tools, tableData, stats }) => { * @param {object[]} tools */ const buildFormatter = (tools) => - tools.map((tool, tIdx) => - tool.columns.map((column, cIdx) => { - const { number_of_significant_digits: sigDigits } = column; - return new NumberFormatterBuilder(sigDigits, `${tIdx}-${cIdx}`); - }), - ); + tools.map((tool, tIdx) => + tool.columns.map((column, cIdx) => { + const { number_of_significant_digits: sigDigits } = column; + return new NumberFormatterBuilder(sigDigits, `${tIdx}-${cIdx}`); + }), + ); const maybeRound = - (key, maxDecimalInputLength, columnIdx) => - (number, { significantDigits }) => { - const asNumber = Number(number); - const [integer, decimal] = number.split("."); - - if (["sum", "avg", "stdev"].includes(key)) { - // for cases when we have no significant digits defined, - // we want to pad avg and stdev to two digits - if (isNil(significantDigits) && key !== "sum") { - return asNumber.toFixed(2); - } - // integer value without leading 0 - const cleanedInt = integer.replace(/^0+/, ""); - // decimal value without leading 0, if cleanedInt is empty (evaluates to zero) - let cleanedDec = decimal || ""; - if (cleanedInt === "") { - cleanedDec = cleanedDec.replace(/^0+/, ""); - } - - // differences in length between input value with maximal length and current value - const deltaInputLength = maxDecimalInputLength - (decimal?.length ?? 0); - - // differences in length between num of significant digits and current value - const deltaSigDigLength = - significantDigits - (cleanedInt.length + cleanedDec.length); - - // if we have not yet filled the number of significant digits, we could decide to pad - const paddingPossible = deltaSigDigLength > 0; - - const missingDigits = (decimal?.length ?? 0) + deltaSigDigLength; - - if (deltaInputLength > 0 && paddingPossible && key !== "stdev") { - if (deltaInputLength > deltaSigDigLength) { - // we want to pad to the smaller value (sigDigits vs maxDecimal) - return asNumber.toFixed(missingDigits); + (key, maxDecimalInputLength, columnIdx) => + (number, { significantDigits }) => { + const asNumber = Number(number); + const [integer, decimal] = number.split("."); + + if (["sum", "avg", "stdev"].includes(key)) { + // for cases when we have no significant digits defined, + // we want to pad avg and stdev to two digits + if (isNil(significantDigits) && key !== "sum") { + return asNumber.toFixed(2); + } + // integer value without leading 0 + const cleanedInt = integer.replace(/^0+/, ""); + // decimal value without leading 0, if cleanedInt is empty (evaluates to zero) + let cleanedDec = decimal || ""; + if (cleanedInt === "") { + cleanedDec = cleanedDec.replace(/^0+/, ""); + } + + // differences in length between input value with maximal length and current value + const deltaInputLength = maxDecimalInputLength - (decimal ? .length ? ? 0); + + // differences in length between num of significant digits and current value + const deltaSigDigLength = + significantDigits - (cleanedInt.length + cleanedDec.length); + + // if we have not yet filled the number of significant digits, we could decide to pad + const paddingPossible = deltaSigDigLength > 0; + + const missingDigits = (decimal ? .length ? ? 0) + deltaSigDigLength; + + if (deltaInputLength > 0 && paddingPossible && key !== "stdev") { + if (deltaInputLength > deltaSigDigLength) { + // we want to pad to the smaller value (sigDigits vs maxDecimal) + return asNumber.toFixed(missingDigits); + } + return asNumber.toFixed(maxDecimalInputLength); + } + + // if avg was previously padded to fill the number of significant digits, + // we want to make sure, that we don't go over the maximumDecimalDigits + if ( + key === "avg" && + !paddingPossible && + deltaInputLength < 0 && + number[number.length - 1] === "0" + ) { + return asNumber.toFixed(maxDecimalInputLength); + } + + if (key === "stdev" && paddingPossible) { + return asNumber.toFixed(missingDigits); + } } - return asNumber.toFixed(maxDecimalInputLength); - } - - // if avg was previously padded to fill the number of significant digits, - // we want to make sure, that we don't go over the maximumDecimalDigits - if ( - key === "avg" && - !paddingPossible && - deltaInputLength < 0 && - number[number.length - 1] === "0" - ) { - return asNumber.toFixed(maxDecimalInputLength); - } - - if (key === "stdev" && paddingPossible) { - return asNumber.toFixed(missingDigits); - } - } - return number; - }; + return number; + }; /** * Used to apply formatting to calculated stats and to remove @@ -212,106 +234,105 @@ const maybeRound = * @param {Function[][]} formatter */ const cleanupStats = (unfilteredStats, formatter, availableStats) => { - const stats = unfilteredStats.map((tool, toolIdx) => - tool.map((col, colIdx) => { - const { columnType } = col; - const out = { columnType }; - - for (const visibleStats of availableStats) { - const currentCol = col[visibleStats]; - if (!currentCol) { - continue; - } - out[visibleStats] = currentCol; - if (currentCol?.sum ?? false) { - formatter[toolIdx][colIdx].addDataItem(currentCol.sum); - } - } - return out; - }), - ); - - for (const to in formatter) { - for (const co in formatter[to]) { - // we build all formatters which makes them ready to use - formatter[to][co] = formatter[to][co].build(); - } - } - - const cleaned = stats.map((tool, toolIdx) => - tool - .map(({ columnType, ...column }, columnIdx) => { - const out = {}; - // if no total is calculated, then no values suitable for calculation were found - if (column.total === undefined) { - return undefined; - } - for (const [resultKey, result] of Object.entries(column)) { - const rowRes = {}; - const meta = result?.meta; - for (let [key, value] of Object.entries(result)) { - // we ignore any of these defined keys - if (keysToIgnore.includes(key)) { - continue; + const stats = unfilteredStats.map((tool, toolIdx) => + tool.map((col, colIdx) => { + const { columnType } = col; + const out = { columnType }; + + for (const visibleStats of availableStats) { + const currentCol = col[visibleStats]; + if (!currentCol) { + continue; + } + out[visibleStats] = currentCol; + if (currentCol ? .sum ? ? false) { + formatter[toolIdx][colIdx].addDataItem(currentCol.sum); + } } + return out; + }), + ); - const maxDecimalInputLength = meta?.maxDecimals ?? 0; + for (const to in formatter) { + for (const co in formatter[to]) { + // we build all formatters which makes them ready to use + formatter[to][co] = formatter[to][co].build(); + } + } - // attach the title to the stat item - // this will later be used to ensure correct ordering of columns - if (key === "title") { - out.title = value; - continue; + const cleaned = stats.map((tool, toolIdx) => + tool + .map(({ columnType, ...column }, columnIdx) => { + const out = {}; + // if no total is calculated, then no values suitable for calculation were found + if (column.total === undefined) { + return undefined; } - // if we have numeric values or 'NaN' we want to apply formatting - if ( - !isNil(value) && - (!isNaN(value) || value === "NaN") && - formatter[toolIdx][columnIdx] - ) { - try { - if (key === "sum") { - rowRes[key] = formatter[toolIdx][columnIdx](value, { - leadingZero: false, - whitespaceFormat: true, - html: true, - additionalFormatting: maybeRound( - key, - maxDecimalInputLength, - columnIdx, - ), - }); - } else { - rowRes[key] = formatter[toolIdx][columnIdx](value, { - leadingZero: true, - whitespaceFormat: false, - html: false, - additionalFormatting: maybeRound( - key, - maxDecimalInputLength, - columnIdx, - ), - }); + for (const [resultKey, result] of Object.entries(column)) { + const rowRes = {}; + const meta = result ? .meta; + for (let [key, value] of Object.entries(result)) { + // we ignore any of these defined keys + if (keysToIgnore.includes(key)) { + continue; + } + + const maxDecimalInputLength = meta ? .maxDecimals ? ? 0; + + // attach the title to the stat item + // this will later be used to ensure correct ordering of columns + if (key === "title") { + out.title = value; + continue; + } + // if we have numeric values or 'NaN' we want to apply formatting + if (!isNil(value) && + (!isNaN(value) || value === "NaN") && + formatter[toolIdx][columnIdx] + ) { + try { + if (key === "sum") { + rowRes[key] = formatter[toolIdx][columnIdx](value, { + leadingZero: false, + whitespaceFormat: true, + html: true, + additionalFormatting: maybeRound( + key, + maxDecimalInputLength, + columnIdx, + ), + }); + } else { + rowRes[key] = formatter[toolIdx][columnIdx](value, { + leadingZero: true, + whitespaceFormat: false, + html: false, + additionalFormatting: maybeRound( + key, + maxDecimalInputLength, + columnIdx, + ), + }); + } + } catch (e) { + console.error({ + key, + value, + formatter: formatter[toolIdx][columnIdx], + e, + }); + } + } } - } catch (e) { - console.error({ - key, - value, - formatter: formatter[toolIdx][columnIdx], - e, - }); - } - } - } - out[resultKey] = rowRes; - } + out[resultKey] = rowRes; + } - return out; - }) - .filter((i) => !isNil(i)), - ); - return cleaned; + return out; + }) + .filter((i) => !isNil(i)), + ); + return cleaned; }; const RESULT_TRUE_PROP = "true"; @@ -327,42 +348,42 @@ const RESULT_CLASS_OTHER = "other"; * @see result.py */ const classifyResult = (result) => { - if (isNil(result)) { + if (isNil(result)) { + return RESULT_CLASS_OTHER; + } + if (result === RESULT_TRUE_PROP) { + return RESULT_CLASS_TRUE; + } + if (result === RESULT_FALSE_PROP) { + return RESULT_CLASS_FALSE; + } + if (result.startsWith(`${RESULT_FALSE_PROP}(`) && result.endsWith(")")) { + return RESULT_CLASS_FALSE; + } + return RESULT_CLASS_OTHER; - } - if (result === RESULT_TRUE_PROP) { - return RESULT_CLASS_TRUE; - } - if (result === RESULT_FALSE_PROP) { - return RESULT_CLASS_FALSE; - } - if (result.startsWith(`${RESULT_FALSE_PROP}(`) && result.endsWith(")")) { - return RESULT_CLASS_FALSE; - } - - return RESULT_CLASS_OTHER; }; const prepareRows = ( - rows, - toolIdx, - categoryAccessor, - statusAccessor, - formatter, + rows, + toolIdx, + categoryAccessor, + statusAccessor, + formatter, ) => { - return rows.map((row) => { - const cat = categoryAccessor(toolIdx, row); - const stat = statusAccessor(toolIdx, row); - - const mappedCat = cat.replace(/-/g, "_"); - const mappedRes = classifyResult(stat); - - return { - categoryType: mappedCat, - resultType: mappedRes, - row: row.results[toolIdx].values, - }; - }); + return rows.map((row) => { + const cat = categoryAccessor(toolIdx, row); + const stat = statusAccessor(toolIdx, row); + + const mappedCat = cat.replace(/-/g, "_"); + const mappedRes = classifyResult(stat); + + return { + categoryType: mappedCat, + resultType: mappedRes, + row: row.results[toolIdx].values, + }; + }); }; /** @@ -372,21 +393,22 @@ const prepareRows = ( * @param {object[]} tools */ const splitColumnsWithMeta = (tools) => (preppedRows, toolIdx) => { - const out = []; - for (const { row, categoryType, resultType } of preppedRows) { - for (const columnIdx in row) { - const column = row[columnIdx].raw; - const curr = out[columnIdx] || []; - // we attach extra meta information for later use in calculation and mapping - // of results - const { type: columnType, title: columnTitle } = - tools[toolIdx].columns[columnIdx]; - - curr.push({ categoryType, resultType, column, columnType, columnTitle }); - out[columnIdx] = curr; + const out = []; + for (const { row, categoryType, resultType } + of preppedRows) { + for (const columnIdx in row) { + const column = row[columnIdx].raw; + const curr = out[columnIdx] || []; + // we attach extra meta information for later use in calculation and mapping + // of results + const { type: columnType, title: columnTitle } = + tools[toolIdx].columns[columnIdx]; + + curr.push({ categoryType, resultType, column, columnType, columnTitle }); + out[columnIdx] = curr; + } } - } - return out; + return out; }; /** @@ -395,37 +417,37 @@ const splitColumnsWithMeta = (tools) => (preppedRows, toolIdx) => { * * @param {object} options */ -const processData = async ({ tools, tableData, formatter }) => { - const catAccessor = (toolIdx, row) => row.results[toolIdx].category; - const statAccessor = (toolIdx, row) => row.results[toolIdx].values[0].raw; - const promises = []; - - const splitRows = []; - for (const toolIdx in tools) { - splitRows.push( - prepareRows(tableData, toolIdx, catAccessor, statAccessor, formatter), - ); - } - const columnSplitter = splitColumnsWithMeta(tools); - - const preparedData = splitRows.map(columnSplitter); - // filter out non-relevant rows - for (const toolIdx in preparedData) { - preparedData[toolIdx] = preparedData[toolIdx].filter((i) => !isNil(i)); - } - - for (const toolDataIdx in preparedData) { - const toolData = preparedData[toolDataIdx]; - const subPromises = []; - for (const columnIdx in toolData) { - const columns = toolData[columnIdx]; - subPromises.push(enqueue({ name: "stats", data: columns })); +const processData = async({ tools, tableData, formatter }) => { + const catAccessor = (toolIdx, row) => row.results[toolIdx].category; + const statAccessor = (toolIdx, row) => row.results[toolIdx].values[0].raw; + const promises = []; + + const splitRows = []; + for (const toolIdx in tools) { + splitRows.push( + prepareRows(tableData, toolIdx, catAccessor, statAccessor, formatter), + ); } - promises[toolDataIdx] = subPromises; - } + const columnSplitter = splitColumnsWithMeta(tools); - const allPromises = promises.map((p) => Promise.all(p)); - const res = await Promise.all(allPromises); + const preparedData = splitRows.map(columnSplitter); + // filter out non-relevant rows + for (const toolIdx in preparedData) { + preparedData[toolIdx] = preparedData[toolIdx].filter((i) => !isNil(i)); + } - return res; -}; + for (const toolDataIdx in preparedData) { + const toolData = preparedData[toolDataIdx]; + const subPromises = []; + for (const columnIdx in toolData) { + const columns = toolData[columnIdx]; + subPromises.push(enqueue({ name: "stats", data: columns })); + } + promises[toolDataIdx] = subPromises; + } + + const allPromises = promises.map((p) => Promise.all(p)); + const res = await Promise.all(allPromises); + + return res; +}; \ No newline at end of file diff --git a/benchexec/tablegenerator/react-table/src/workers/scripts/stats.worker.js b/benchexec/tablegenerator/react-table/src/workers/scripts/stats.worker.js index f0a9a8d64..0f79b0443 100644 --- a/benchexec/tablegenerator/react-table/src/workers/scripts/stats.worker.js +++ b/benchexec/tablegenerator/react-table/src/workers/scripts/stats.worker.js @@ -15,42 +15,42 @@ * @returns {Number} The result of the addition */ const safeAdd = (a, b) => { - let aNum = a; - let bNum = b; - - if (typeof a === "string") { - aNum = Number(a); - } - if (typeof b === "string") { - bNum = Number(b); - } - - if (Number.isInteger(aNum) || Number.isInteger(bNum)) { - return aNum + bNum; - } - - const aString = a.toString(); - const aLength = aString.length; - const aDecimalPoint = aString.indexOf("."); - const bString = b.toString(); - const bLength = bString.length; - const bDecimalPoint = bString.indexOf("."); - - const length = Math.max(aLength - aDecimalPoint, bLength - bDecimalPoint) - 1; - - return Number((aNum + bNum).toFixed(length)); + let aNum = a; + let bNum = b; + + if (typeof a === "string") { + aNum = Number(a); + } + if (typeof b === "string") { + bNum = Number(b); + } + + if (Number.isInteger(aNum) || Number.isInteger(bNum)) { + return aNum + bNum; + } + + const aString = a.toString(); + const aLength = aString.length; + const aDecimalPoint = aString.indexOf("."); + const bString = b.toString(); + const bLength = bString.length; + const bDecimalPoint = bString.indexOf("."); + + const length = Math.max(aLength - aDecimalPoint, bLength - bDecimalPoint) - 1; + + return Number((aNum + bNum).toFixed(length)); }; const mathStringMax = (a, b) => { - const numA = Number(a); - const numB = Number(b); - return numA > numB ? a : b; + const numA = Number(a); + const numB = Number(b); + return numA > numB ? a : b; }; const mathStringMin = (a, b) => { - const numA = Number(a); - const numB = Number(b); - return numA < numB ? a : b; + const numA = Number(a); + const numB = Number(b); + return numA < numB ? a : b; }; /** @@ -64,72 +64,72 @@ const mathStringMin = (a, b) => { * @param {String} type */ const maybeAdd = (a, b, type) => { - if (Number(b)) { - return safeAdd(a, b); - } - if (type === "status") { - return a + 1; - } - return a; + if (Number(b)) { + return safeAdd(a, b); + } + if (type === "status") { + return a + 1; + } + return a; }; const removeRoundOff = (num) => { - const str = num.toString(); - if (str.match(/\..+?0{2,}\d$/)) { - return Number(str.substr(0, str.length - 1)); - } - return num; + const str = num.toString(); + if (str.match(/\..+?0{2,}\d$/)) { + return Number(str.substr(0, str.length - 1)); + } + return num; }; const calculateMean = (values, allItems) => { - const numMin = Number(values.min); - const numMax = Number(values.max); - if (numMin === -Infinity && numMax === Infinity) { - values.avg = "NaN"; - } else if (numMin === -Infinity) { - values.avg = "-Infinity"; - } else if (numMax === Infinity) { - values.avg = "Infinity"; - } else { - values.avg = removeRoundOff(values.sum / allItems.length); - } + const numMin = Number(values.min); + const numMax = Number(values.max); + if (numMin === -Infinity && numMax === Infinity) { + values.avg = "NaN"; + } else if (numMin === -Infinity) { + values.avg = "-Infinity"; + } else if (numMax === Infinity) { + values.avg = "Infinity"; + } else { + values.avg = removeRoundOff(values.sum / allItems.length); + } }; const calculateMedian = (values, allItems) => { - if (allItems.length % 2 === 0) { - const idx = allItems.length / 2; - values.median = - (Number(allItems[idx - 1].column) + Number(allItems[idx].column)) / 2.0; - } else { - values.median = allItems[Math.floor(allItems.length / 2.0)].column; - } + if (allItems.length % 2 === 0) { + const idx = allItems.length / 2; + values.median = + (Number(allItems[idx - 1].column) + Number(allItems[idx].column)) / 2.0; + } else { + values.median = allItems[Math.floor(allItems.length / 2.0)].column; + } }; const calculateStdev = (hasNegInf, hasPosInf, variance, size) => { - if (hasNegInf && hasPosInf) { - return "NaN"; - } - if (hasNegInf || hasPosInf) { - return Infinity; - } - return Math.sqrt(variance / size); + if (hasNegInf && hasPosInf) { + return "NaN"; + } + if (hasNegInf || hasPosInf) { + return Infinity; + } + return Math.sqrt(variance / size); }; const parsePythonInfinityValues = (data) => - data.map((item) => { - if (item.columnType === "status" || !item.column.endsWith("Inf")) { - return item; - } - // We have a python Infinity value that we want to transfer to a string - // that can be interpreted as a JavaScript Infinity value - item.column = item.column.replace("Inf", "Infinity"); - return item; - }); + data.map((item) => { + if (item.columnType === "status" || !item.column.endsWith("Inf")) { + return item; + } + // We have a python Infinity value that we want to transfer to a string + // that can be interpreted as a JavaScript Infinity value + item.column = item.column.replace("Inf", "Infinity"); + return item; + }); // If a bucket contains a NaN value, we can not perform any stat calculation const shouldSkipBucket = (bucketMeta, key) => { - if (bucketMeta[key] && bucketMeta[key].hasNaN) { - return true; - } - return false; + if (bucketMeta[key] && bucketMeta[key].hasNaN) { + return true; + } + return false; }; /** @@ -145,13 +145,13 @@ const shouldSkipBucket = (bucketMeta, key) => { * @param {UpdateMaxDecimalMetaInfoParam} param */ const updateMaxDecimalMetaInfo = ({ columnType, column, bucket }) => { - if (columnType !== "status") { - const [, decimal] = column.split("."); - bucket.meta.maxDecimals = Math.max( - bucket.meta.maxDecimals, - decimal?.length ?? 0, - ); - } + if (columnType !== "status") { + const [, decimal] = column.split("."); + bucket.meta.maxDecimals = Math.max( + bucket.meta.maxDecimals, + decimal ? .length ? ? 0, + ); + } }; /** @@ -178,243 +178,259 @@ const updateMaxDecimalMetaInfo = ({ columnType, column, bucket }) => { * @prop {MetaInfo} [meta] - Meta information of the bucket */ -onmessage = function (e) { - const { data, transaction } = e.data; - - // template - /** @const { Bucket } */ - const defaultObj = { - sum: 0, - avg: 0, - max: "-Infinity", - median: 0, - min: "Infinity", - stdev: 0, - variance: 0, - }; - - /** @const {MetaInfo} */ - const metaTemplate = { - type: null, - maxDecimals: 0, - }; - - // Copy of the template with all values replaced with NaN - const nanObj = { ...defaultObj }; - for (const objKey of Object.keys(nanObj)) { - nanObj[objKey] = "NaN"; - } - - let copy = [...data].filter( - (i) => i && i.column !== undefined && i.column !== null, - ); - copy = parsePythonInfinityValues(copy); - - if (copy.length === 0) { - // No data to perform calculations with - postResult({ total: undefined }, transaction); - return; - } - - const { columnType } = copy[0]; - metaTemplate.type = columnType; - - copy.sort((a, b) => a.column - b.column); - - /** @type {Object.} */ - const buckets = {}; - const bucketNaNInfo = {}; // used to store NaN info of buckets - - /** @type {Bucket} */ - let total = { ...defaultObj, items: [], meta: { ...metaTemplate } }; - - total.max = copy[copy.length - 1].column; - total.min = copy[0].column; - - const totalNaNInfo = { - hasNaN: copy.some((item) => { - if (item.columnType !== "status" && isNaN(item.column)) { - return true; - } - return false; - }), - }; - - // Bucket setup with sum and min/max - for (const item of copy) { - const key = `${item.categoryType}_${item.resultType}`; - const totalKey = `${item.categoryType}`; - const { columnType: type, column, columnTitle: title } = item; - if (!total.title) { - total.title = title; - } - const bucket = buckets[key] || { - ...defaultObj, - title, - items: [], - meta: { ...metaTemplate }, +onmessage = function(e) { + const { data, transaction } = e.data; + + // template + /** @const { Bucket } */ + const defaultObj = { + sum: 0, + avg: 0, + max: "-Infinity", + median: 0, + min: "Infinity", + stdev: 0, + variance: 0, }; - const subTotalBucket = buckets[totalKey] || { - ...defaultObj, - title, - items: [], - meta: { ...metaTemplate }, + /** @const {MetaInfo} */ + const metaTemplate = { + type: null, + maxDecimals: 0, }; - const itemIsNaN = type !== "status" && isNaN(column); - - // if one item is NaN we store that info so we can default all - // calculated values for this bucket to NaN - if (itemIsNaN) { - bucketNaNInfo[key] = { hasNaN: true }; - bucketNaNInfo[totalKey] = { hasNaN: true }; + const customStatusCounts = {}; + const bucket = buckets[key] || {...defaultObj, items: [], meta: {...metaTemplate } }; - // set all values for this bucket to NaN - buckets[key] = { ...nanObj, title }; - buckets[totalKey] = { ...nanObj, title }; - continue; + // Copy of the template with all values replaced with NaN + const nanObj = {...defaultObj }; + for (const objKey of Object.keys(nanObj)) { + nanObj[objKey] = "NaN"; } - // we check if we should skip calculation for these buckets - const skipBucket = shouldSkipBucket(bucketNaNInfo, key); - const skipSubTotal = shouldSkipBucket(bucketNaNInfo, totalKey); - - if (!skipBucket) { - bucket.sum = maybeAdd(bucket.sum, column, type); - updateMaxDecimalMetaInfo({ columnType, column, bucket }); - } - if (!skipSubTotal) { - subTotalBucket.sum = maybeAdd(subTotalBucket.sum, column, type); - updateMaxDecimalMetaInfo({ columnType, column, bucket: subTotalBucket }); + let copy = [...data].filter( + (i) => i && i.column !== undefined && i.column !== null, + ); + copy = parsePythonInfinityValues(copy); + + for (const item of copy) { + const key = `${item.categoryType}_${item.resultType}`; + const status = item.resultType; + const bucket = buckets[key] || {...defaultObj, items: [], meta: {...metaTemplate } }; + + if (!customStatusCounts[status]) { + customStatusCounts[status] = 0; + } + customStatusCounts[status] += 1; + buckets[key] = bucket; } - if (!totalNaNInfo.hasNaN) { - total.sum = maybeAdd(total.sum, column, type); - updateMaxDecimalMetaInfo({ columnType, column, bucket: total }); + if (copy.length === 0) { + // No data to perform calculations with + const result = { columnType, total, ...buckets }; + result.customStatusCounts = customStatusCounts; + postResult({ total: undefined }, transaction); + return; } - if (!isNaN(Number(column))) { - if (!skipBucket) { - bucket.max = mathStringMax(bucket.max, column); - bucket.min = mathStringMin(bucket.min, column); - } - if (!skipSubTotal) { - subTotalBucket.max = mathStringMax(subTotalBucket.max, column); - subTotalBucket.min = mathStringMin(subTotalBucket.min, column); - } - } - if (!skipBucket) { - try { - bucket.items.push(item); - } catch (e) { - console.e({ bucket, bucketMeta: bucketNaNInfo, key }); - } - } - if (!skipSubTotal) { - try { - subTotalBucket.items.push(item); - } catch (e) { - console.e({ subTotalBucket, bucketMeta: bucketNaNInfo, totalKey }); - } + const { columnType } = copy[0]; + metaTemplate.type = columnType; + + copy.sort((a, b) => a.column - b.column); + + /** @type {Object.} */ + const buckets = {}; + const bucketNaNInfo = {}; // used to store NaN info of buckets + + /** @type {Bucket} */ + let total = {...defaultObj, items: [], meta: {...metaTemplate } }; + + total.max = copy[copy.length - 1].column; + total.min = copy[0].column; + + const totalNaNInfo = { + hasNaN: copy.some((item) => { + if (item.columnType !== "status" && isNaN(item.column)) { + return true; + } + return false; + }), + }; + + // Bucket setup with sum and min/max + for (const item of copy) { + const key = `${item.categoryType}_${item.resultType}`; + const totalKey = `${item.categoryType}`; + const { columnType: type, column, columnTitle: title } = item; + if (!total.title) { + total.title = title; + } + const bucket = buckets[key] || { + ...defaultObj, + title, + items: [], + meta: {...metaTemplate }, + }; + + const subTotalBucket = buckets[totalKey] || { + ...defaultObj, + title, + items: [], + meta: {...metaTemplate }, + }; + + const itemIsNaN = type !== "status" && isNaN(column); + + // if one item is NaN we store that info so we can default all + // calculated values for this bucket to NaN + if (itemIsNaN) { + bucketNaNInfo[key] = { hasNaN: true }; + bucketNaNInfo[totalKey] = { hasNaN: true }; + + // set all values for this bucket to NaN + buckets[key] = {...nanObj, title }; + buckets[totalKey] = {...nanObj, title }; + continue; + } + + // we check if we should skip calculation for these buckets + const skipBucket = shouldSkipBucket(bucketNaNInfo, key); + const skipSubTotal = shouldSkipBucket(bucketNaNInfo, totalKey); + + if (!skipBucket) { + bucket.sum = maybeAdd(bucket.sum, column, type); + updateMaxDecimalMetaInfo({ columnType, column, bucket }); + } + if (!skipSubTotal) { + subTotalBucket.sum = maybeAdd(subTotalBucket.sum, column, type); + updateMaxDecimalMetaInfo({ columnType, column, bucket: subTotalBucket }); + } + if (!totalNaNInfo.hasNaN) { + total.sum = maybeAdd(total.sum, column, type); + updateMaxDecimalMetaInfo({ columnType, column, bucket: total }); + } + + if (!isNaN(Number(column))) { + if (!skipBucket) { + bucket.max = mathStringMax(bucket.max, column); + bucket.min = mathStringMin(bucket.min, column); + } + if (!skipSubTotal) { + subTotalBucket.max = mathStringMax(subTotalBucket.max, column); + subTotalBucket.min = mathStringMin(subTotalBucket.min, column); + } + } + if (!skipBucket) { + try { + bucket.items.push(item); + } catch (e) { + console.e({ bucket, bucketMeta: bucketNaNInfo, key }); + } + } + if (!skipSubTotal) { + try { + subTotalBucket.items.push(item); + } catch (e) { + console.e({ subTotalBucket, bucketMeta: bucketNaNInfo, totalKey }); + } + } + + buckets[key] = bucket; + buckets[totalKey] = subTotalBucket; } - buckets[key] = bucket; - buckets[totalKey] = subTotalBucket; - } + for (const [bucket, values] of Object.entries(buckets)) { + if (shouldSkipBucket(bucketNaNInfo, bucket)) { + continue; + } + calculateMean(values, values.items); - for (const [bucket, values] of Object.entries(buckets)) { - if (shouldSkipBucket(bucketNaNInfo, bucket)) { - continue; + calculateMedian(values, values.items); + buckets[bucket] = values; } - calculateMean(values, values.items); - - calculateMedian(values, values.items); - buckets[bucket] = values; - } - const totalHasNaN = totalNaNInfo.hasNaN; - - if (totalHasNaN) { - total = { ...total, ...nanObj }; - } else { - calculateMean(total, copy); - calculateMedian(total, copy); - } - - for (const item of copy) { - const { column } = item; - if (isNaN(Number(column))) { - continue; + const totalHasNaN = totalNaNInfo.hasNaN; + + if (totalHasNaN) { + total = {...total, ...nanObj }; + } else { + calculateMean(total, copy); + calculateMedian(total, copy); } - const numCol = Number(column); - const key = `${item.categoryType}_${item.resultType}`; - const totalKey = `${item.categoryType}`; - const bucket = buckets[key]; - const subTotalBucket = buckets[totalKey]; - const diffBucket = numCol - bucket.avg; - const diffSubTotal = numCol - subTotalBucket.avg; - const diffTotal = numCol - total.avg; - total.variance += Math.pow(diffTotal, 2); - bucket.variance += Math.pow(diffBucket, 2); - subTotalBucket.variance += Math.pow(diffSubTotal, 2); - } - - const totalHasNegInf = Number(total.min) === -Infinity; - const totalHasPosInf = Number(total.max) === Infinity; - total.stdev = calculateStdev( - totalHasNegInf, - totalHasPosInf, - total.variance, - copy.length, - ); - - for (const [bucket, values] of Object.entries(buckets)) { - if (shouldSkipBucket(bucketNaNInfo, bucket)) { - for (const [key, val] of Object.entries(values)) { - values[key] = val.toString(); - } - buckets[bucket] = values; - continue; + + for (const item of copy) { + const { column } = item; + if (isNaN(Number(column))) { + continue; + } + const numCol = Number(column); + const key = `${item.categoryType}_${item.resultType}`; + const totalKey = `${item.categoryType}`; + const bucket = buckets[key]; + const subTotalBucket = buckets[totalKey]; + const diffBucket = numCol - bucket.avg; + const diffSubTotal = numCol - subTotalBucket.avg; + const diffTotal = numCol - total.avg; + total.variance += Math.pow(diffTotal, 2); + bucket.variance += Math.pow(diffBucket, 2); + subTotalBucket.variance += Math.pow(diffSubTotal, 2); } - const valuesHaveNegInf = Number(values.min) === -Infinity; - const valuesHavePosInf = Number(total.max) === Infinity; - values.stdev = calculateStdev( - valuesHaveNegInf, - valuesHavePosInf, - values.variance, - values.items.length, + + const totalHasNegInf = Number(total.min) === -Infinity; + const totalHasPosInf = Number(total.max) === Infinity; + total.stdev = calculateStdev( + totalHasNegInf, + totalHasPosInf, + total.variance, + copy.length, ); - for (const [key, val] of Object.entries(values)) { - if (key === "meta") { - continue; - } - values[key] = val.toString(); + for (const [bucket, values] of Object.entries(buckets)) { + if (shouldSkipBucket(bucketNaNInfo, bucket)) { + for (const [key, val] of Object.entries(values)) { + values[key] = val.toString(); + } + buckets[bucket] = values; + continue; + } + const valuesHaveNegInf = Number(values.min) === -Infinity; + const valuesHavePosInf = Number(total.max) === Infinity; + values.stdev = calculateStdev( + valuesHaveNegInf, + valuesHavePosInf, + values.variance, + values.items.length, + ); + + for (const [key, val] of Object.entries(values)) { + if (key === "meta") { + continue; + } + values[key] = val.toString(); + } + // clearing memory + delete values.items; + delete values.variance; + buckets[bucket] = values; } - // clearing memory - delete values.items; - delete values.variance; - buckets[bucket] = values; - } - - for (const [key, value] of Object.entries(total)) { - if (key === "meta") { - continue; + + for (const [key, value] of Object.entries(total)) { + if (key === "meta") { + continue; + } + total[key] = value.toString(); } - total[key] = value.toString(); - } - delete total.items; - delete total.variance; + delete total.items; + delete total.variance; - const result = { columnType, total, ...buckets }; - postResult(result, transaction); + const result = { columnType, total, ...buckets }; + postResult(result, transaction); }; const postResult = (result, transaction) => { - // handling in tests - if (this.mockedPostMessage) { - this.mockedPostMessage({ result, transaction }); - return; - } - postMessage({ result, transaction }); -}; + // handling in tests + if (this.mockedPostMessage) { + this.mockedPostMessage({ result, transaction }); + return; + } + postMessage({ result, transaction }); +}; \ No newline at end of file