diff --git a/app/appearance/langs/de_DE.json b/app/appearance/langs/de_DE.json index 2d5ad6ae2a3..be4f6f99a0e 100644 --- a/app/appearance/langs/de_DE.json +++ b/app/appearance/langs/de_DE.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "NICHT LEER", "calcResultPercentEmpty": "LEER", "calcResultPercentNotEmpty": "NICHT LEER", + "calcResultPercentUniqueValues": "Prozent einzigartige Werte", "calcResultSum": "SUMME", "calcResultAverage": "DURCHSCHNITT", "calcResultMedian": "MEDIAN", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "Nicht leer zählen", "calcOperatorPercentEmpty": "Prozent leer", "calcOperatorPercentNotEmpty": "Prozent nicht leer", + "calcOperatorPercentUniqueValues": "Prozent einzigartige Werte", "calcOperatorSum": "Summe", "calcOperatorAverage": "Durchschnitt", "calcOperatorMedian": "Median", diff --git a/app/appearance/langs/en_US.json b/app/appearance/langs/en_US.json index bc62c35cabe..c859910d370 100644 --- a/app/appearance/langs/en_US.json +++ b/app/appearance/langs/en_US.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "NOT EMPTY", "calcResultPercentEmpty": "EMPTY", "calcResultPercentNotEmpty": "NOT EMPTY", + "calcResultPercentUniqueValues": "Percent unique values", "calcResultSum": "SUM", "calcResultAverage": "AVERAGE", "calcResultMedian": "MEDIAN", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "Count not empty", "calcOperatorPercentEmpty": "Percent empty", "calcOperatorPercentNotEmpty": "Percent not empty", + "calcOperatorPercentUniqueValues": "Percent unique values", "calcOperatorSum": "Sum", "calcOperatorAverage": "Average", "calcOperatorMedian": "Median", diff --git a/app/appearance/langs/es_ES.json b/app/appearance/langs/es_ES.json index e363199a29f..486723607cf 100644 --- a/app/appearance/langs/es_ES.json +++ b/app/appearance/langs/es_ES.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "NO VACÍO", "calcResultPercentEmpty": "VACÍO", "calcResultPercentNotEmpty": "NO VACÍO", + "calcResultPercentUniqueValues": "Porcentaje de valores únicos", "calcResultSum": "SUMA", "calcResultAverage": "PROMEDIO", "calcResultMedian": "MEDIANA", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "Cuenta no vacía", "calcOperatorPercentEmpty": "Porcentaje vacío", "calcOperatorPercentNotEmpty": "Porcentaje no vacío", + "calcOperatorPercentUniqueValues": "Porcentaje de valores únicos", "calcOperatorSum": "Suma", "calcOperatorAverage": "Promedio", "calcOperatorMedian": "Mediana", diff --git a/app/appearance/langs/fr_FR.json b/app/appearance/langs/fr_FR.json index 547f15f3a8f..faf345df59d 100644 --- a/app/appearance/langs/fr_FR.json +++ b/app/appearance/langs/fr_FR.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "NON VIDE", "calcResultPercentEmpty": "VIDE", "calcResultPercentNotEmpty": "NON VIDE", + "calcResultPercentUniqueValues": "Pourcentage de valeurs uniques", "calcResultSum": "SOMME", "calcResultAverage": "MOYENNE", "calcResultMedian": "MÉDIANE", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "Compter non vide", "calcOperatorPercentEmpty": "Pourcentage vide", "calcOperatorPercentNotEmpty": "Pourcentage non vide", + "calcOperatorPercentUniqueValues": "Pourcentage de valeurs uniques", "calcOperatorSum": "Somme", "calcOperatorAverage": "Moyenne", "calcOperatorMedian": "Médiane", diff --git a/app/appearance/langs/he_IL.json b/app/appearance/langs/he_IL.json index 334715c7fab..329d6e856c0 100644 --- a/app/appearance/langs/he_IL.json +++ b/app/appearance/langs/he_IL.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "לא ריק", "calcResultPercentEmpty": "ריק", "calcResultPercentNotEmpty": "לא ריק", + "calcResultPercentUniqueValues": "אחוז ערכים ייחודיים", "calcResultSum": "סכום", "calcResultAverage": "ממוצע", "calcResultMedian": "חציון", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "ספור לא ריקים", "calcOperatorPercentEmpty": "אחוז ריקים", "calcOperatorPercentNotEmpty": "אחוז לא ריקים", + "calcOperatorPercentUniqueValues": "אחוז ערכים ייחודיים", "calcOperatorSum": "סכום", "calcOperatorAverage": "ממוצע", "calcOperatorMedian": "חציון", diff --git a/app/appearance/langs/it_IT.json b/app/appearance/langs/it_IT.json index 75d491886b1..e9bd4429c9b 100644 --- a/app/appearance/langs/it_IT.json +++ b/app/appearance/langs/it_IT.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "NON VUOTO", "calcResultPercentEmpty": "VUOTO", "calcResultPercentNotEmpty": "NON VUOTO", + "calcResultPercentUniqueValues": "Percentuale di valori unici", "calcResultSum": "SOMMA", "calcResultAverage": "MEDIA", "calcResultMedian": "MEDIANA", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "Conta non vuoti", "calcOperatorPercentEmpty": "Percentuale vuoti", "calcOperatorPercentNotEmpty": "Percentuale non vuoti", + "calcOperatorPercentUniqueValues": "Percentuale di valori unici", "calcOperatorSum": "Somma", "calcOperatorAverage": "Media", "calcOperatorMedian": "Mediana", diff --git a/app/appearance/langs/ja_JP.json b/app/appearance/langs/ja_JP.json index 8b545572bbd..47800fbd4e3 100644 --- a/app/appearance/langs/ja_JP.json +++ b/app/appearance/langs/ja_JP.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "空ではない数", "calcResultPercentEmpty": "空のパーセント", "calcResultPercentNotEmpty": "空ではないパーセント", + "calcResultPercentUniqueValues": "ユニーク値の割合", "calcResultSum": "合計値", "calcResultAverage": "平均値", "calcResultMedian": "中央値", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "空ではない数", "calcOperatorPercentEmpty": "空のパーセント", "calcOperatorPercentNotEmpty": "空ではないパーセント", + "calcOperatorPercentUniqueValues": "ユニーク値の割合", "calcOperatorSum": "合計値", "calcOperatorAverage": "平均値", "calcOperatorMedian": "中央値", diff --git a/app/appearance/langs/pl_PL.json b/app/appearance/langs/pl_PL.json index cfabd3dcd5d..957f1b2f79d 100644 --- a/app/appearance/langs/pl_PL.json +++ b/app/appearance/langs/pl_PL.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "NIE PUSTY", "calcResultPercentEmpty": "PUSTE", "calcResultPercentNotEmpty": "NIE PUSTE", + "calcResultPercentUniqueValues": "Procent unikalnych wartości", "calcResultSum": "SUMA", "calcResultAverage": "ŚREDNIA", "calcResultMedian": "MEDIANA", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "Zlicz niepuste", "calcOperatorPercentEmpty": "Procent pustych", "calcOperatorPercentNotEmpty": "Procent niepustych", + "calcOperatorPercentUniqueValues": "Procent unikalnych wartości", "calcOperatorSum": "Suma", "calcOperatorAverage": "Średnia", "calcOperatorMedian": "Mediana", diff --git a/app/appearance/langs/ru_RU.json b/app/appearance/langs/ru_RU.json index 64d2b29f351..7cfe6c8faf4 100644 --- a/app/appearance/langs/ru_RU.json +++ b/app/appearance/langs/ru_RU.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "НЕ ПУСТО", "calcResultPercentEmpty": "ПУСТО", "calcResultPercentNotEmpty": "НЕ ПУСТО", + "calcResultPercentUniqueValues": "Процент уникальных значений", "calcResultSum": "СУММА", "calcResultAverage": "СРЕДНЕЕ", "calcResultMedian": "МЕДИАНА", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "Подсчитать непустые", "calcOperatorPercentEmpty": "Процент пустых", "calcOperatorPercentNotEmpty": "Процент не пустых", + "calcOperatorPercentUniqueValues": "Процент уникальных значений", "calcOperatorSum": "Сумма", "calcOperatorAverage": "Среднее", "calcOperatorMedian": "Медиана", diff --git a/app/appearance/langs/zh_CHT.json b/app/appearance/langs/zh_CHT.json index e39dadf5412..4c3910f5d1a 100644 --- a/app/appearance/langs/zh_CHT.json +++ b/app/appearance/langs/zh_CHT.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "已填寫", "calcResultPercentEmpty": "未填寫佔比", "calcResultPercentNotEmpty": "已填寫佔比", + "calcResultPercentUniqueValues": "唯一值佔比", "calcResultSum": "求和", "calcResultAverage": "平均值", "calcResultMedian": "中位數", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "已填寫", "calcOperatorPercentEmpty": "未填寫佔比", "calcOperatorPercentNotEmpty": "已填寫佔比", + "calcOperatorPercentUniqueValues": "唯一值佔比", "calcOperatorSum": "求和", "calcOperatorAverage": "平均值", "calcOperatorMedian": "中位數", diff --git a/app/appearance/langs/zh_CN.json b/app/appearance/langs/zh_CN.json index 9a060b97f9a..da133b4dca6 100644 --- a/app/appearance/langs/zh_CN.json +++ b/app/appearance/langs/zh_CN.json @@ -239,6 +239,7 @@ "calcResultCountNotEmpty": "已填写", "calcResultPercentEmpty": "未填写占比", "calcResultPercentNotEmpty": "已填写占比", + "calcResultPercentUniqueValues": "唯一值占比", "calcResultSum": "求和", "calcResultAverage": "平均值", "calcResultMedian": "中位数", @@ -256,6 +257,7 @@ "calcOperatorCountNotEmpty": "已填写", "calcOperatorPercentEmpty": "未填写占比", "calcOperatorPercentNotEmpty": "已填写占比", + "calcOperatorPercentUniqueValues": "唯一值占比", "calcOperatorSum": "求和", "calcOperatorAverage": "平均值", "calcOperatorMedian": "中位数", diff --git a/app/src/protyle/render/av/calc.ts b/app/src/protyle/render/av/calc.ts index 4d570675a6c..cdb2b3ef1ba 100644 --- a/app/src/protyle/render/av/calc.ts +++ b/app/src/protyle/render/av/calc.ts @@ -208,6 +208,17 @@ export const openCalcMenu = async (protyle: IProtyle, calcElement: HTMLElement, blockID, target: calcElement }); + calcItem({ + menu, + protyle, + colId, + avId, + oldOperator, + operator: "Percent unique values", + data: panelData?.data, + blockID, + target: calcElement + }); } else { calcItem({ menu, @@ -427,6 +438,9 @@ export const getCalcValue = (column: IAVColumn) => { case "Percent not empty": value = `${resultCalc.formattedContent}${window.siyuan.languages.calcResultPercentNotEmpty}`; break; + case "Percent unique values": + value = `${resultCalc.formattedContent}${window.siyuan.languages.calcResultPercentUniqueValues}`; + break; case "Sum": value = `${resultCalc.formattedContent}${window.siyuan.languages.calcResultSum}`; break; @@ -486,6 +500,8 @@ export const getNameByOperator = (operator: string, isRollup: boolean) => { return window.siyuan.languages.calcOperatorPercentEmpty; case "Percent not empty": return window.siyuan.languages.calcOperatorPercentNotEmpty; + case "Percent unique values": + return window.siyuan.languages.calcOperatorPercentUniqueValues; case "Checked": return window.siyuan.languages.checked; case "Unchecked": diff --git a/kernel/av/calc.go b/kernel/av/calc.go index 4260fcdda2e..b312cae11e3 100644 --- a/kernel/av/calc.go +++ b/kernel/av/calc.go @@ -28,24 +28,25 @@ type ColumnCalc struct { type CalcOperator string const ( - CalcOperatorNone CalcOperator = "" - CalcOperatorCountAll CalcOperator = "Count all" - CalcOperatorCountValues CalcOperator = "Count values" - CalcOperatorCountUniqueValues CalcOperator = "Count unique values" - CalcOperatorCountEmpty CalcOperator = "Count empty" - CalcOperatorCountNotEmpty CalcOperator = "Count not empty" - CalcOperatorPercentEmpty CalcOperator = "Percent empty" - CalcOperatorPercentNotEmpty CalcOperator = "Percent not empty" - CalcOperatorSum CalcOperator = "Sum" - CalcOperatorAverage CalcOperator = "Average" - CalcOperatorMedian CalcOperator = "Median" - CalcOperatorMin CalcOperator = "Min" - CalcOperatorMax CalcOperator = "Max" - CalcOperatorRange CalcOperator = "Range" - CalcOperatorEarliest CalcOperator = "Earliest" - CalcOperatorLatest CalcOperator = "Latest" - CalcOperatorChecked CalcOperator = "Checked" - CalcOperatorUnchecked CalcOperator = "Unchecked" - CalcOperatorPercentChecked CalcOperator = "Percent checked" - CalcOperatorPercentUnchecked CalcOperator = "Percent unchecked" + CalcOperatorNone CalcOperator = "" + CalcOperatorCountAll CalcOperator = "Count all" + CalcOperatorCountValues CalcOperator = "Count values" + CalcOperatorCountUniqueValues CalcOperator = "Count unique values" + CalcOperatorCountEmpty CalcOperator = "Count empty" + CalcOperatorCountNotEmpty CalcOperator = "Count not empty" + CalcOperatorPercentEmpty CalcOperator = "Percent empty" + CalcOperatorPercentNotEmpty CalcOperator = "Percent not empty" + CalcOperatorPercentUniqueValues CalcOperator = "Percent unique values" + CalcOperatorSum CalcOperator = "Sum" + CalcOperatorAverage CalcOperator = "Average" + CalcOperatorMedian CalcOperator = "Median" + CalcOperatorMin CalcOperator = "Min" + CalcOperatorMax CalcOperator = "Max" + CalcOperatorRange CalcOperator = "Range" + CalcOperatorEarliest CalcOperator = "Earliest" + CalcOperatorLatest CalcOperator = "Latest" + CalcOperatorChecked CalcOperator = "Checked" + CalcOperatorUnchecked CalcOperator = "Unchecked" + CalcOperatorPercentChecked CalcOperator = "Percent checked" + CalcOperatorPercentUnchecked CalcOperator = "Percent unchecked" ) diff --git a/kernel/av/table_calc.go b/kernel/av/table_calc.go index acd5ec68e5d..52fb18cc0a6 100644 --- a/kernel/av/table_calc.go +++ b/kernel/av/table_calc.go @@ -130,6 +130,20 @@ func (table *Table) calcColTemplate(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Template && "" != row.Cells[colIndex].Value.Template.Content { + if !uniqueValues[row.Cells[colIndex].Value.Template.Content] { + uniqueValues[row.Cells[colIndex].Value.Template.Content] = true + countUniqueValues++ + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } case CalcOperatorSum: sum := 0.0 for _, row := range table.Rows { @@ -276,6 +290,22 @@ func (table *Table) calcColMAsset(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.MAsset && 0 < len(row.Cells[colIndex].Value.MAsset) { + for _, sel := range row.Cells[colIndex].Value.MAsset { + if _, ok := uniqueValues[sel.Content]; !ok { + uniqueValues[sel.Content] = true + countUniqueValues++ + } + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } } } @@ -341,6 +371,22 @@ func (table *Table) calcColMSelect(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.MSelect && 0 < len(row.Cells[colIndex].Value.MSelect) { + for _, sel := range row.Cells[colIndex].Value.MSelect { + if _, ok := uniqueValues[sel.Content]; !ok { + uniqueValues[sel.Content] = true + countUniqueValues++ + } + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } } } @@ -404,6 +450,20 @@ func (table *Table) calcColSelect(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.MSelect && 0 < len(row.Cells[colIndex].Value.MSelect) && nil != row.Cells[colIndex].Value.MSelect[0] && "" != row.Cells[colIndex].Value.MSelect[0].Content { + if _, ok := uniqueValues[row.Cells[colIndex].Value.MSelect[0].Content]; !ok { + uniqueValues[row.Cells[colIndex].Value.MSelect[0].Content] = true + countUniqueValues++ + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } } } @@ -467,6 +527,20 @@ func (table *Table) calcColDate(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[int64]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Date && row.Cells[colIndex].Value.Date.IsNotEmpty { + if _, ok := uniqueValues[row.Cells[colIndex].Value.Date.Content]; !ok { + countUniqueValues++ + uniqueValues[row.Cells[colIndex].Value.Date.Content] = true + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } case CalcOperatorEarliest: earliest := int64(0) var isNotTime, hasEndDate bool @@ -581,6 +655,20 @@ func (table *Table) calcColNumber(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[float64]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Number && row.Cells[colIndex].Value.Number.IsNotEmpty { + if !uniqueValues[row.Cells[colIndex].Value.Number.Content] { + uniqueValues[row.Cells[colIndex].Value.Number.Content] = true + countUniqueValues++ + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } case CalcOperatorSum: sum := 0.0 for _, row := range table.Rows { @@ -719,6 +807,20 @@ func (table *Table) calcColText(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Text && "" != row.Cells[colIndex].Value.Text.Content { + if !uniqueValues[row.Cells[colIndex].Value.Text.Content] { + uniqueValues[row.Cells[colIndex].Value.Text.Content] = true + countUniqueValues++ + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } } } @@ -782,6 +884,20 @@ func (table *Table) calcColURL(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.URL && "" != row.Cells[colIndex].Value.URL.Content { + if !uniqueValues[row.Cells[colIndex].Value.URL.Content] { + uniqueValues[row.Cells[colIndex].Value.URL.Content] = true + countUniqueValues++ + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } } } @@ -845,6 +961,20 @@ func (table *Table) calcColEmail(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Email && "" != row.Cells[colIndex].Value.Email.Content { + if !uniqueValues[row.Cells[colIndex].Value.Email.Content] { + uniqueValues[row.Cells[colIndex].Value.Email.Content] = true + countUniqueValues++ + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } } } @@ -908,6 +1038,20 @@ func (table *Table) calcColPhone(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Phone && "" != row.Cells[colIndex].Value.Phone.Content { + if !uniqueValues[row.Cells[colIndex].Value.Phone.Content] { + uniqueValues[row.Cells[colIndex].Value.Phone.Content] = true + countUniqueValues++ + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } } } @@ -971,6 +1115,20 @@ func (table *Table) calcColBlock(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Block && "" != row.Cells[colIndex].Value.Block.Content { + if !uniqueValues[row.Cells[colIndex].Value.Block.Content] { + uniqueValues[row.Cells[colIndex].Value.Block.Content] = true + countUniqueValues++ + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } } } @@ -1034,6 +1192,20 @@ func (table *Table) calcColCreated(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[int64]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Created { + if _, ok := uniqueValues[row.Cells[colIndex].Value.Created.Content]; !ok { + countUniqueValues++ + uniqueValues[row.Cells[colIndex].Value.Created.Content] = true + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } case CalcOperatorEarliest: earliest := int64(0) for _, row := range table.Rows { @@ -1137,6 +1309,20 @@ func (table *Table) calcColUpdated(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[int64]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Updated && row.Cells[colIndex].Value.Updated.IsNotEmpty { + if _, ok := uniqueValues[row.Cells[colIndex].Value.Updated.Content]; !ok { + countUniqueValues++ + uniqueValues[row.Cells[colIndex].Value.Updated.Content] = true + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } case CalcOperatorEarliest: earliest := int64(0) for _, row := range table.Rows { @@ -1285,6 +1471,22 @@ func (table *Table) calcColRelation(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Relation { + for _, id := range row.Cells[colIndex].Value.Relation.BlockIDs { + if !uniqueValues[id] { + uniqueValues[id] = true + countUniqueValues++ + } + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } } } @@ -1350,6 +1552,22 @@ func (table *Table) calcColRollup(col *TableColumn, colIndex int) { if 0 < len(table.Rows) { col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countNotEmpty)/float64(len(table.Rows)), NumberFormatPercent)} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, row := range table.Rows { + if nil != row.Cells[colIndex] && nil != row.Cells[colIndex].Value && nil != row.Cells[colIndex].Value.Rollup { + for _, content := range row.Cells[colIndex].Value.Rollup.Contents { + if !uniqueValues[content.String(true)] { + uniqueValues[content.String(true)] = true + countUniqueValues++ + } + } + } + } + if 0 < len(table.Rows) { + col.Calc.Result = &Value{Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(table.Rows)), NumberFormatPercent)} + } case CalcOperatorSum: sum := 0.0 for _, row := range table.Rows { diff --git a/kernel/av/value.go b/kernel/av/value.go index bc00f0ee8b7..b50d133efba 100644 --- a/kernel/av/value.go +++ b/kernel/av/value.go @@ -730,6 +730,18 @@ func (r *ValueRollup) RenderContents(calc *RollupCalc, destKey *Key) { if 0 < len(r.Contents) { r.Contents = []*Value{{Type: KeyTypeNumber, Number: NewFormattedValueNumber(float64(countNonEmpty)/float64(len(r.Contents)), NumberFormatPercent)}} } + case CalcOperatorPercentUniqueValues: + countUniqueValues := 0 + uniqueValues := map[string]bool{} + for _, v := range r.Contents { + if _, ok := uniqueValues[v.String(true)]; !ok { + uniqueValues[v.String(true)] = true + countUniqueValues++ + } + } + if 0 < len(r.Contents) { + r.Contents = []*Value{{Type: KeyTypeNumber, Number: NewFormattedValueNumber(float64(countUniqueValues)/float64(len(r.Contents)), NumberFormatPercent)}} + } case CalcOperatorSum: sum := 0.0 for _, v := range r.Contents {