From 8e47487a23f31520d4066ce200f752778a97e2db Mon Sep 17 00:00:00 2001 From: despiegk Date: Sun, 20 Jul 2025 06:21:14 +0200 Subject: [PATCH] ... --- examples/biztools/bizmodel.vsh | 6 +- examples/biztools/bizmodel1.vsh | 28 ++++ .../bizmodel_docusaurus/docusaurus/README.md | 1 - .../bizmodel_docusaurus/docusaurus/build.sh | 22 --- .../docusaurus/cfg/footer.json | 1 - .../docusaurus/cfg/main.json | 1 - .../docusaurus/cfg/navbar.json | 1 - .../bizmodel_docusaurus/docusaurus/develop.sh | 16 -- examples/biztools/test_revenue_model.v | 28 ++++ lib/biz/bizmodel/docu/revenue.md | 13 +- lib/biz/bizmodel/play.v | 21 ++- .../spreadsheet/docu/charting_capabilities.md | 94 ++++++++++++ .../docu/data_aggregation_transformation.md | 93 ++++++++++++ lib/biz/spreadsheet/docu/exporting_data.md | 44 ++++++ lib/biz/spreadsheet/docu/overview.md | 51 +++++++ .../spreadsheet/docu/row_cell_operations.md | 66 +++++++++ lib/biz/spreadsheet/docu/sheet_operations.md | 88 +++++++++++ lib/biz/spreadsheet/number.v | 3 + lib/biz/spreadsheet/sheet_pprint.v | 137 ++++++++++++++++++ 19 files changed, 660 insertions(+), 54 deletions(-) create mode 100755 examples/biztools/bizmodel1.vsh delete mode 100644 examples/biztools/bizmodel_docusaurus/docusaurus/README.md delete mode 100755 examples/biztools/bizmodel_docusaurus/docusaurus/build.sh delete mode 100644 examples/biztools/bizmodel_docusaurus/docusaurus/cfg/footer.json delete mode 100644 examples/biztools/bizmodel_docusaurus/docusaurus/cfg/main.json delete mode 100644 examples/biztools/bizmodel_docusaurus/docusaurus/cfg/navbar.json delete mode 100755 examples/biztools/bizmodel_docusaurus/docusaurus/develop.sh create mode 100644 examples/biztools/test_revenue_model.v create mode 100644 lib/biz/spreadsheet/docu/charting_capabilities.md create mode 100644 lib/biz/spreadsheet/docu/data_aggregation_transformation.md create mode 100644 lib/biz/spreadsheet/docu/exporting_data.md create mode 100644 lib/biz/spreadsheet/docu/overview.md create mode 100644 lib/biz/spreadsheet/docu/row_cell_operations.md create mode 100644 lib/biz/spreadsheet/docu/sheet_operations.md create mode 100644 lib/biz/spreadsheet/sheet_pprint.v diff --git a/examples/biztools/bizmodel.vsh b/examples/biztools/bizmodel.vsh index 1ed37de5..cb0af49c 100755 --- a/examples/biztools/bizmodel.vsh +++ b/examples/biztools/bizmodel.vsh @@ -11,9 +11,9 @@ buildpath := '${os.home_dir()}/hero/var/mdbuild/bizmodel' mut model := bizmodel.generate('test', playbook_path)! println(model.sheet) -println(model.sheet.export()!) +// println(model.sheet.export()!) -model.sheet.export(path: '~/Downloads/test.csv')! +model.sheet.pprint()! // model.sheet.export(path: '~/code/github/freeflowuniverse/starlight_template/src/content/test.csv')! +// model.sheet -model.sheet diff --git a/examples/biztools/bizmodel1.vsh b/examples/biztools/bizmodel1.vsh new file mode 100755 index 00000000..9108b0e0 --- /dev/null +++ b/examples/biztools/bizmodel1.vsh @@ -0,0 +1,28 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.biz.bizmodel +import os + +heroscript:=" + +Next will define an OEM product in month 10, 1 Million EUR, ... cogs is a percent which is 20% at start but goes to 10% after 20 months. + +!!bizmodel.revenue_define bizname:'test' name:'oem1' + descr:'OEM Deals' + revenue:'10:1000000EUR,15:3333,20:1200000' + cogs_percent: '1:20%,20:10%' + + +This time we have the cogs defined in fixed manner, the default currency is USD doesn't have to be mentioned. + +!!bizmodel.revenue_define bizname:'test' name:'oem2' + descr:'OEM Deals' + revenue:'10:1000000EUR,15:3333,20:1200000' + cogs: '10:100000,15:1000,20:120000' +" + +bizmodel.play(heroscript:heroscript)! + +mut bm:=bizmodel.get("test")! + +bm.sheet.pprint()! \ No newline at end of file diff --git a/examples/biztools/bizmodel_docusaurus/docusaurus/README.md b/examples/biztools/bizmodel_docusaurus/docusaurus/README.md deleted file mode 100644 index c84f20c0..00000000 --- a/examples/biztools/bizmodel_docusaurus/docusaurus/README.md +++ /dev/null @@ -1 +0,0 @@ -output dir of example \ No newline at end of file diff --git a/examples/biztools/bizmodel_docusaurus/docusaurus/build.sh b/examples/biztools/bizmodel_docusaurus/docusaurus/build.sh deleted file mode 100755 index 0a7708e5..00000000 --- a/examples/biztools/bizmodel_docusaurus/docusaurus/build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -set -ex - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "${script_dir}" - -echo "Docs directory: $script_dir" - -cd "${HOME}/hero/var/docusaurus" - -export PATH=/tmp/docusaurus_build/node_modules/.bin:${HOME}/.bun/bin/:$PATH - -rm -rf /Users/despiegk/hero/var/docusaurus/build/ - -. ${HOME}/.zprofile - -bun docusaurus build - -mkdir -p /Users/despiegk/code/github/freeflowuniverse/herolib/examples/biztools/bizmodel/example/docusaurus -echo SYNC TO /Users/despiegk/code/github/freeflowuniverse/herolib/examples/biztools/bizmodel/example/docusaurus -rsync -rv --delete /Users/despiegk/hero/var/docusaurus/build/ /Users/despiegk/code/github/freeflowuniverse/herolib/examples/biztools/bizmodel/example/docusaurus/ diff --git a/examples/biztools/bizmodel_docusaurus/docusaurus/cfg/footer.json b/examples/biztools/bizmodel_docusaurus/docusaurus/cfg/footer.json deleted file mode 100644 index 1c40e241..00000000 --- a/examples/biztools/bizmodel_docusaurus/docusaurus/cfg/footer.json +++ /dev/null @@ -1 +0,0 @@ -{"style":"dark","links":[]} \ No newline at end of file diff --git a/examples/biztools/bizmodel_docusaurus/docusaurus/cfg/main.json b/examples/biztools/bizmodel_docusaurus/docusaurus/cfg/main.json deleted file mode 100644 index 3a84b4ae..00000000 --- a/examples/biztools/bizmodel_docusaurus/docusaurus/cfg/main.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"","title":"Docusaurus","tagline":"","favicon":"img/favicon.png","url":"http://localhost","url_home":"docs/introduction","baseUrl":"/","image":"img/tf_graph.png","metadata":{"description":"Docusaurus","image":"Docusaurus","title":"Docusaurus"},"buildDest":[],"buildDestDev":[]} \ No newline at end of file diff --git a/examples/biztools/bizmodel_docusaurus/docusaurus/cfg/navbar.json b/examples/biztools/bizmodel_docusaurus/docusaurus/cfg/navbar.json deleted file mode 100644 index e172b568..00000000 --- a/examples/biztools/bizmodel_docusaurus/docusaurus/cfg/navbar.json +++ /dev/null @@ -1 +0,0 @@ -{"title":"Business Model","items":[{"href":"https://threefold.info/kristof/","label":"ThreeFold Technology","position":"right"},{"href":"https://threefold.io","label":"Operational Plan","position":"left"}]} \ No newline at end of file diff --git a/examples/biztools/bizmodel_docusaurus/docusaurus/develop.sh b/examples/biztools/bizmodel_docusaurus/docusaurus/develop.sh deleted file mode 100755 index 673e34b6..00000000 --- a/examples/biztools/bizmodel_docusaurus/docusaurus/develop.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "${script_dir}" - -echo "Docs directory: $script_dir" - -cd "${HOME}/hero/var/docusaurus" - -export PATH=/tmp/docusaurus_build/node_modules/.bin:${HOME}/.bun/bin/:$PATH - -. ${HOME}/.zprofile - -bun run start -p 3100 diff --git a/examples/biztools/test_revenue_model.v b/examples/biztools/test_revenue_model.v new file mode 100644 index 00000000..90a96dd7 --- /dev/null +++ b/examples/biztools/test_revenue_model.v @@ -0,0 +1,28 @@ +import freeflowuniverse.herolib.biz.bizmodel +import os + +fn main() ! { + heroscript:=" + + Next will define an OEM product in month 10, 1 Million EUR, ... cogs is a percent which is 20% at start but goes to 10% after 20 months. + + !!bizmodel.revenue_define bizname:'test' name:'oem1' + descr:'OEM Deals' + revenue:'10:1000000EUR,15:3333,20:1200000' + cogs_percent: '1:20%,20:10%' + + + This time we have the cogs defined in fixed manner, the default currency is USD doesn't have to be mentioned. + + !!bizmodel.revenue_define bizname:'test' name:'oem2' + descr:'OEM Deals' + revenue:'10:1000000EUR,15:3333,20:1200000' + cogs: '10:100000,15:1000,20:120000' + " + + bizmodel.play(heroscript:heroscript)! + + mut bm:=bizmodel.get("test")! + + bm.sheet.pprint()! +} \ No newline at end of file diff --git a/lib/biz/bizmodel/docu/revenue.md b/lib/biz/bizmodel/docu/revenue.md index 498d4367..fb5b52b0 100644 --- a/lib/biz/bizmodel/docu/revenue.md +++ b/lib/biz/bizmodel/docu/revenue.md @@ -50,7 +50,10 @@ follow rows in sheets ```v -heroscript:=' +import freeflowuniverse.herolib.biz.bizmodel +import os + +heroscript:=" Next will define an OEM product in month 10, 1 Million EUR, ... cogs is a percent which is 20% at start but goes to 10% after 20 months. @@ -58,7 +61,7 @@ Next will define an OEM product in month 10, 1 Million EUR, ... cogs is a percen descr:'OEM Deals' revenue:'10:1000000EUR,15:3333,20:1200000' cogs_percent: '1:20%,20:10%' -' + This time we have the cogs defined in fixed manner, the default currency is USD doesn't have to be mentioned. @@ -66,8 +69,12 @@ This time we have the cogs defined in fixed manner, the default currency is USD descr:'OEM Deals' revenue:'10:1000000EUR,15:3333,20:1200000' cogs: '10:100000,15:1000,20:120000' -' +" +bizmodel.play(heroscript:heroscript)! +mut bm:=bizmodel.get("test")! + +bm.sheet.pprint()! ``` \ No newline at end of file diff --git a/lib/biz/bizmodel/play.v b/lib/biz/bizmodel/play.v index 0025473e..660e8aa5 100644 --- a/lib/biz/bizmodel/play.v +++ b/lib/biz/bizmodel/play.v @@ -1,12 +1,8 @@ module bizmodel import arrays -import freeflowuniverse.herolib.core.texttools -import freeflowuniverse.herolib.core.playbook { Action, PlayBook } +import freeflowuniverse.herolib.core.playbook { PlayBook, Action } import freeflowuniverse.herolib.ui.console -// import freeflowuniverse.herolib.core.texttools -// import freeflowuniverse.herolib.ui.console -import freeflowuniverse.herolib.biz.spreadsheet const action_priorities = { 0: ['revenue_define', 'costcenter_define', 'funding_define'] @@ -14,7 +10,20 @@ const action_priorities = { 2: ['sheet_wiki', 'graph_bar_row', 'graph_pie_row', 'graph_line_row', 'row_overview'] } -pub fn play(mut plbook PlayBook) ! { +@[params] +pub struct PlayArgs { +pub mut: + heroscript string + heroscript_path string + plbook ?PlayBook + reset bool +} + +pub fn play(args_ PlayArgs) ! { + mut args := args_ + mut plbook := args.plbook or { + playbook.new(text: args.heroscript, path: args.heroscript_path)! + } // group actions by which bizmodel they belong to actions_by_biz := arrays.group_by[string, &Action](plbook.actions_find(actor: 'bizmodel')!, fn (a &Action) string { diff --git a/lib/biz/spreadsheet/docu/charting_capabilities.md b/lib/biz/spreadsheet/docu/charting_capabilities.md new file mode 100644 index 00000000..cde23a3f --- /dev/null +++ b/lib/biz/spreadsheet/docu/charting_capabilities.md @@ -0,0 +1,94 @@ +# Charting Capabilities + +The spreadsheet module integrates with ECharts to provide powerful data visualization capabilities, allowing you to generate various types of charts directly from your sheet data. + +## Common Charting Parameters (`RowGetArgs`) + +Most charting functions utilize a common set of arguments defined in `RowGetArgs` to specify which data to chart and how to present it. + +**Key `RowGetArgs` Parameters:** +- `rowname` (string, optional): The exact name of a single row to chart. +- `namefilter` ([]string, optional): A list of exact row names to include. +- `includefilter` ([]string, optional): A list of tags to include. Rows must match at least one of these tags. +- `excludefilter` ([]string, optional): A list of tags to exclude. +- `period_type` (`PeriodType`, optional): Specifies the period type for the chart's X-axis (e.g., `.month`, `.quarter`, `.year`). +- `aggregate` (bool, optional, default: `true`): If `true` and multiple rows match the filters, their values will be aggregated. +- `aggregatetype` (`RowAggregateType`, optional, default: `.sum`): The type of aggregation to perform if `aggregate` is `true`. +- `unit` (`UnitType`, optional): The unit of the data (e.g., currency, percentage). +- `title` (string, optional): The main title of the chart. +- `title_sub` (string, optional): A subtitle for the chart. +- `size` (string, optional): For pie charts, this can define the radius (e.g., "50%"). +- `rowname_show` (bool, optional, default: `true`): Whether to show the row name in the chart legend/labels. +- `descr_show` (bool, optional, default: `false`): Whether to show the row description. If `true`, `rowname_show` will be set to `false`. +- `description` (string, optional): A general description for the chart. + +## Chart Types + +### Line Chart (`line_chart`) + +Generates a line chart, ideal for visualizing trends over time. Multiple rows can be plotted on the same chart. + +```v +import freeflowuniverse.herolib.biz.spreadsheet +import freeflowuniverse.herolib.web.echarts + +// Assuming 'my_sheet' is an existing Sheet object +mut my_sheet := spreadsheet.sheet_new(name: 'my_sheet', nrcol: 60)! +// ... populate my_sheet with data + +// Generate a line chart for 'revenue_row' and 'expenses_row' over months +line_chart_option := my_sheet.line_chart( + rowname: 'revenue_row,expenses_row', // Comma-separated row names + period_type: .month, + title: 'Revenue vs. Expenses Over Time', + title_sub: 'Monthly Data' +)! + +// The 'line_chart_option' can then be used to render the chart using ECharts. +``` + +### Bar Chart (`bar_chart`) + +Generates a bar chart, suitable for comparing discrete categories or values. Typically used for a single row's data. + +```v +import freeflowuniverse.herolib.biz.spreadsheet +import freeflowuniverse.herolib.web.echarts + +// Assuming 'my_sheet' is an existing Sheet object +mut my_sheet := spreadsheet.sheet_new(name: 'my_sheet', nrcol: 60)! +// ... populate my_sheet with data + +// Generate a bar chart for 'profit_row' aggregated by quarter +bar_chart_option := my_sheet.bar_chart( + rowname: 'profit_row', + period_type: .quarter, + title: 'Quarterly Profit', + title_sub: 'Aggregated Data' +)! +``` + +### Pie Chart (`pie_chart`) + +Generates a pie chart, useful for showing the proportion of different categories within a single data set. Typically used for a single row's data. + +```v +import freeflowuniverse.herolib.biz.spreadsheet +import freeflowuniverse.herolib.web.echarts + +// Assuming 'my_sheet' is an existing Sheet object +mut my_sheet := spreadsheet.sheet_new(name: 'my_sheet', nrcol: 60)! +// ... populate my_sheet with data + +// Generate a pie chart for 'budget_allocation_row' showing yearly distribution +pie_chart_option := my_sheet.pie_chart( + rowname: 'budget_allocation_row', + period_type: .year, + title: 'Annual Budget Allocation', + size: '70%' // Set the radius of the pie chart +)! +``` + +## Integration with ECharts + +The charting functions return an `echarts.EChartsOption` object, which is a JSON-serializable structure compatible with the ECharts JavaScript library. This allows for flexible rendering of these charts in web interfaces or other environments that support ECharts. \ No newline at end of file diff --git a/lib/biz/spreadsheet/docu/data_aggregation_transformation.md b/lib/biz/spreadsheet/docu/data_aggregation_transformation.md new file mode 100644 index 00000000..7e3b842e --- /dev/null +++ b/lib/biz/spreadsheet/docu/data_aggregation_transformation.md @@ -0,0 +1,93 @@ +# Data Aggregation and Transformation + +The spreadsheet module provides powerful functionalities to aggregate and transform data within sheets, allowing for different views and summarized insights. + +## Grouping Rows into a New Row + +The `group2row` method allows you to select rows based on their tags and aggregate their values into a new, single row. This is particularly useful for creating summary rows (e.g., total salaries for a specific department). + +```v +import freeflowuniverse.herolib.biz.spreadsheet + +// Assuming 'my_sheet' is an existing Sheet object with various rows +// and some rows have tags like 'department:dev' or 'department:engineering'. +mut my_sheet := spreadsheet.sheet_new(name: 'my_sheet', nrcol: 60)! +// ... add rows to my_sheet with relevant tags and data + +// Aggregate all rows tagged with 'department:dev' or 'department:engineering' +// into a new row named 'total_dev_engineering_salaries'. +// The aggregation type defaults to 'sum'. +mut total_salaries_row := my_sheet.group2row( + name: 'total_dev_engineering_salaries', + include: ['department:dev', 'department:engineering'], + tags: 'summary:dev_eng', // Optional: tags for the new aggregated row + descr: 'Total salaries for Development and Engineering departments' +)! + +// You can also specify an aggregation type other than sum, e.g., .avg, .max, .min +// mut avg_salaries_row := my_sheet.group2row( +// name: 'average_salaries', +// include: ['department:dev'], +// aggregatetype: .avg +// )! +``` + +**`Group2RowArgs` Parameters:** +- `name` (string, required): The name of the new aggregated row. +- `include` ([]string, optional): A list of tags to include. Rows must match at least one of these tags to be included in the aggregation. Supports wildcard matching (e.g., `location:belgium_*`). +- `exclude` ([]string, optional): A list of tags to exclude. Rows matching any of these tags will be excluded from the aggregation. +- `tags` (string, optional): Tags to assign to the newly created aggregated row. +- `descr` (string, optional): Description for the new aggregated row. +- `subgroup` (string, optional): Subgroup for the new aggregated row. +- `aggregatetype` (`RowAggregateType`, optional, default: `.sum`): The type of aggregation to perform. + - `.sum`: Sums the values of matching cells. + - `.avg`: Calculates the average of matching cells. + - `.max`: Finds the maximum value among matching cells. + - `.min`: Finds the minimum value among matching cells. + +## Transforming Sheet Periodicity (Yearly/Quarterly Aggregation) + +The module allows you to create new sheets where the data is aggregated into larger time periods, such as years or quarters, from a monthly-based sheet. + +### Aggregating to Yearly Data + +The `toyear` method creates a new sheet where monthly data is aggregated into yearly columns. + +```v +import freeflowuniverse.herolib.biz.spreadsheet + +// Assuming 'monthly_sheet' is a sheet with 60 columns (5 years of monthly data) +mut monthly_sheet := spreadsheet.sheet_new(name: 'monthly_data', nrcol: 60)! +// ... populate monthly_sheet with data + +// Create a new sheet 'yearly_data' with data aggregated by year +mut yearly_sheet := monthly_sheet.toyear( + name: 'yearly_data', + namefilter: ['revenue_row', 'expenses_row'], // Optional: only include specific rows + includefilter: ['category:income'], // Optional: filter rows by tags + excludefilter: ['status:draft'] // Optional: exclude rows by tags +)! +``` + +### Aggregating to Quarterly Data + +Similarly, the `toquarter` method creates a new sheet with data aggregated into quarterly columns. + +```v +import freeflowuniverse.herolib.biz.spreadsheet + +// Assuming 'monthly_sheet' is a sheet with 60 columns +mut monthly_sheet := spreadsheet.sheet_new(name: 'monthly_data', nrcol: 60)! +// ... populate monthly_sheet with data + +// Create a new sheet 'quarterly_data' with data aggregated by quarter +mut quarterly_sheet := monthly_sheet.toquarter( + name: 'quarterly_data' +)! +``` + +**`ToYearQuarterArgs` Parameters:** +- `name` (string, optional): The name of the new aggregated sheet. If empty, a default name based on the original sheet and period (e.g., `original_sheet_name_year`) is used. +- `namefilter` ([]string, optional): A list of exact row names to include in the new sheet. If provided, only these rows will be processed. +- `includefilter` ([]string, optional): A list of tags to include. Rows must match at least one of these tags to be included. +- `excludefilter` ([]string, optional): A list of tags to exclude. Rows matching any of these tags will be excluded. \ No newline at end of file diff --git a/lib/biz/spreadsheet/docu/exporting_data.md b/lib/biz/spreadsheet/docu/exporting_data.md new file mode 100644 index 00000000..1e47b097 --- /dev/null +++ b/lib/biz/spreadsheet/docu/exporting_data.md @@ -0,0 +1,44 @@ +# Exporting Data + +The spreadsheet module provides functionality to export sheet data into various formats, making it easy to integrate with other tools or for reporting purposes. + +## Exporting to CSV + +The `export_csv` method allows you to export the sheet's data to a CSV (Comma Separated Values) file or directly as a string. It offers flexibility in terms of separators and handling of empty cells. + +```v +import freeflowuniverse.herolib.biz.spreadsheet +import os + +// Assuming 'my_sheet' is an existing Sheet object +mut my_sheet := spreadsheet.sheet_new(name: 'my_sheet', nrcol: 60)! +// ... populate my_sheet with data + +// Export to a CSV file with the default pipe '|' separator +// The file will be created at '~/output.csv' +my_sheet.export_csv(path: '~/output.csv')! + +// Export to a CSV file with a custom comma ',' separator and include empty cells +csv_content_with_empty := my_sheet.export_csv( + path: '~/output_with_empty.csv', + separator: ',', + include_empty: true +)! + +// Export to a string only (no file will be created) +csv_string := my_sheet.export_csv(path: '')! +println(csv_string) +``` + +**`ExportCSVArgs` Parameters:** +- `path` (string, optional): The file path where the CSV should be saved. If an empty string is provided, the CSV content will be returned as a string instead of being written to a file. The `~` character is expanded to the user's home directory. +- `include_empty` (bool, optional, default: `false`): If `true`, empty cells will be included in the CSV output (represented as '0' for numeric values, or empty string for others). If `false`, empty cells will be represented as an empty string. +- `separator` (string, optional, default: `'|'`): The character used to separate values in the CSV. Common separators include ',' (comma), ';' (semicolon), or '|' (pipe). + +**CSV Export Features:** +- **Configurable Separator:** Easily change the delimiter to suit different CSV parsing requirements. +- **Special Character Handling:** Values containing the separator, double quotes, or newlines are automatically enclosed in double quotes, and internal double quotes are escaped (e.g., `"` becomes `""`). +- **Empty Cell Inclusion:** Control whether empty cells are explicitly represented in the output. +- **Numeric Formatting:** Numeric values are formatted appropriately, with small numbers showing up to 3 decimal places and larger numbers as integers. Trailing zeros and decimal points are removed if unnecessary. + +The CSV output includes a header row with "Name", "Description", "AggregateType", "Tags", "Subgroup", followed by the sheet's column headers (e.g., "M1", "M2", "Y1", etc.). Each subsequent row represents a sheet row, with its metadata followed by its cell values. \ No newline at end of file diff --git a/lib/biz/spreadsheet/docu/overview.md b/lib/biz/spreadsheet/docu/overview.md new file mode 100644 index 00000000..635cf4d7 --- /dev/null +++ b/lib/biz/spreadsheet/docu/overview.md @@ -0,0 +1,51 @@ +# Spreadsheet Module Overview + +The `spreadsheet` module in `herolib` provides a software representation of a spreadsheet, designed for business modeling and data analysis. It supports multi-currency behavior, powerful extrapolation/interpolation, and various data manipulation and visualization capabilities. + +## Core Concepts + +### Sheet +A `Sheet` is the primary container, representing the entire spreadsheet. It holds a collection of `Row` objects and defines global properties such as the number of columns (`nrcol`), visualization parameters (`SheetParams`), and the associated currency. + +**Key Properties:** +- `name`: A unique identifier for the sheet. +- `rows`: A map of `Row` objects, indexed by their names. +- `nrcol`: The number of columns in the sheet, typically representing periods (e.g., 60 months for 5 years). +- `params`: Configuration parameters for the sheet, such as `visualize_cur` (whether to display currency symbols). +- `currency`: The default currency for the sheet, used for currency exchange operations. + +### Row +A `Row` represents a single horizontal line of data within a `Sheet`. Each row has a name, an optional alias, a description, and can be associated with tags for grouping and filtering. It contains a series of `Cell` objects, each holding a value for a specific column. + +**Key Properties:** +- `name`: A unique identifier for the row within the sheet. +- `alias`: An optional alternative name for the row. +- `description`: A textual description of the row's purpose. +- `tags`: A string of space-separated tags used for categorization and filtering (e.g., "location:belgium_*, department:finance"). +- `cells`: A list of `Cell` objects, representing the data points across columns. +- `aggregatetype`: Defines how values in this row should be aggregated (e.g., sum, average, min, max). + +### Cell +A `Cell` is the fundamental unit of data in the spreadsheet, residing at the intersection of a row and a column. It stores a floating-point value (`f64`) and a flag indicating if it's empty. + +**Key Properties:** +- `val`: The numeric value stored in the cell. +- `empty`: A boolean flag, `true` if the cell is empty, `false` otherwise. + +**Key Operations:** +- `set(v string)`: Sets the cell's value. This method intelligently handles currency inputs, converting them to the sheet's base currency if necessary. +- `add(v f64)`: Adds a numeric value to the existing cell value. +- `repr()`: Returns a string representation of the cell's value, displaying '-' for empty cells and formatting numbers appropriately. + +## Multi-Currency Support + +The spreadsheet module inherently supports multi-currency operations. When a value is set in a cell using a currency string (e.g., "100 EUR"), the system automatically converts it to the sheet's defined currency based on exchange rates. + +## Period Representation + +Sheets can represent data over various periods: +- **Months:** Typically 60 columns for a 5-year period. +- **Years:** Data can be aggregated into yearly columns. +- **Quarters:** Data can be aggregated into quarterly columns. + +This flexibility allows for different levels of granularity in financial and business modeling. \ No newline at end of file diff --git a/lib/biz/spreadsheet/docu/row_cell_operations.md b/lib/biz/spreadsheet/docu/row_cell_operations.md new file mode 100644 index 00000000..df45a6b7 --- /dev/null +++ b/lib/biz/spreadsheet/docu/row_cell_operations.md @@ -0,0 +1,66 @@ +# Row and Cell Operations + +Rows and Cells are fundamental building blocks of the spreadsheet, allowing for granular data management and manipulation. + +## Row Operations + +### Creating a New Row + +Rows are created within the context of a `Sheet` object. + +```v +import freeflowuniverse.herolib.biz.spreadsheet + +// Assuming 'my_sheet' is an existing Sheet object +mut my_sheet := spreadsheet.sheet_new(name: 'my_sheet', nrcol: 60)! + +// Create a new row named 'salaries' with specific tags and description +mut salaries_row := my_sheet.row_new( + name: 'salaries', + tags: 'department:hr location:belgium', + descr: 'Monthly salaries for HR department in Belgium', + aggregatetype: .sum // Optional: define default aggregation for this row +)! +``` + +### Setting Cell Values in a Row + +Once a row is created, you can set values for its individual cells. The `set` method of a `Cell` handles currency conversion automatically if a currency string is provided. + +```v +// Set the value of the first cell (column 0) in 'salaries_row' +// The value "1000 USD" will be converted to the sheet's currency if different. +salaries_row.cells[0].set('1000 USD')! + +// Set a plain numeric value for the second cell (column 1) +salaries_row.cells[1].set('1200.50')! +``` + +### Adding Values to Cells + +You can add a numeric value to an existing cell's value. + +```v +// Add 500 to the value of the first cell +salaries_row.cells[0].add(500.0) +``` + +### Retrieving Row Values + +You can get all the values of a row as a list of `f64`. + +```v +// Get all values from 'salaries_row' +values := salaries_row.values_get() +// values will be a []f64 +``` + +### String Representation of a Cell + +The `repr()` and `str()` methods of a `Cell` provide a formatted string representation of its value. + +```v +// Assuming 'my_cell' is a Cell object +cell_string := my_cell.repr() +// If the cell is empty, cell_string will be "-" +// Otherwise, it will be the formatted numeric value. \ No newline at end of file diff --git a/lib/biz/spreadsheet/docu/sheet_operations.md b/lib/biz/spreadsheet/docu/sheet_operations.md new file mode 100644 index 00000000..7d84cb7b --- /dev/null +++ b/lib/biz/spreadsheet/docu/sheet_operations.md @@ -0,0 +1,88 @@ +# Sheet Operations + +The `Sheet` object is the central component of the spreadsheet module, providing methods to manage and manipulate the overall spreadsheet data. + +## Creating a New Sheet + +A new sheet can be initialized using the `sheet_new` function. + +```v +import freeflowuniverse.herolib.biz.spreadsheet + +// Create a new sheet named 'my_financial_sheet' with 60 columns +mut my_sheet := spreadsheet.sheet_new( + name: 'my_financial_sheet', + nrcol: 60, + visualize_cur: true, // Optional: display currency symbols in cells + curr: 'USD' // Optional: set the default currency for the sheet +)! +``` + +## Retrieving Rows and Cells + +You can access specific rows and cells within a sheet using their names and column numbers. + +### Get a Row by Name + +```v +// Assuming 'my_sheet' is an existing Sheet object +// and 'revenue_row' is the name of a row within it. +mut revenue_row := my_sheet.row_get('revenue_row')! +// Now you can work with the 'revenue_row' object +``` + +### Get a Cell by Row Name and Column Number + +```v +// Get the cell at column 5 of 'revenue_row' +mut cell_data := my_sheet.cell_get('revenue_row', 5)! +// Access the value: cell_data.val +``` + +## Deleting Rows + +Rows can be removed from a sheet using their name. + +```v +// Delete the row named 'expense_row' +my_sheet.row_delete('expense_row') +// Alternatively, using the 'delete' alias +my_sheet.delete('expense_row') +``` + +## Calculating Widths for Display + +The sheet provides utility functions to determine the maximum string length of cell values, row names, and row descriptions, which can be useful for formatting output. + +### Maximum Cell Width for a Column + +```v +// Get the maximum width of cells in the first column (column index 0) +max_width_col0 := my_sheet.cells_width(0)! +``` + +### Maximum Row Name/Alias Width + +```v +// Get the maximum width among all row names and aliases in the sheet +max_row_name_width := my_sheet.rows_names_width_max() +``` + +### Maximum Row Description Width + +```v +// Get the maximum width among all row descriptions in the sheet +max_row_descr_width := my_sheet.rows_description_width_max() +``` + +## Generating Headers + +The `header()` function generates a list of strings representing the column headers based on the number of columns (`nrcol`). It automatically determines if the sheet represents months, quarters, or years. + +```v +// For a sheet with 60 columns (months) +// header_labels will be ["M1", "M2", ..., "M60"] +header_labels := my_sheet.header()! + +// For a sheet with 5 columns (years) +// header_labels will be ["Y1", "Y2", ..., "Y5"] \ No newline at end of file diff --git a/lib/biz/spreadsheet/number.v b/lib/biz/spreadsheet/number.v index 9f07df69..6247dfb6 100644 --- a/lib/biz/spreadsheet/number.v +++ b/lib/biz/spreadsheet/number.v @@ -9,6 +9,9 @@ pub enum ReprType { // represent a pub fn float_repr(nr_ f64, reprtype ReprType) string { + if nr_ == 0.0 { + return '' + } mut out := '' mut nr := nr_ mut nr_pos := math.abs(nr) diff --git a/lib/biz/spreadsheet/sheet_pprint.v b/lib/biz/spreadsheet/sheet_pprint.v new file mode 100644 index 00000000..1fe23693 --- /dev/null +++ b/lib/biz/spreadsheet/sheet_pprint.v @@ -0,0 +1,137 @@ +module spreadsheet + +import os +import math +import strconv + +// pad_right pads a string on the right with spaces to a specified length +fn pad_right(s string, length int) string { + if s.len >= length { + return s + } + mut res := s + for _ in 0 .. length - s.len { + res += ' ' + } + return res +} + +@[params] +pub struct PPrintArgs { +pub mut: + group_months int = 1 //e.g. if 2 then will group by 2 months + nr_columns int = 0 //number of columns to show in the table, 0 is all + description bool //show description in the table + aggrtype bool = true //show aggregate type in the table + tags bool //show tags in the table + subgroup bool //show subgroup in the table +} +// calculate_column_widths calculates the maximum width for each column +fn calculate_column_widths(rows [][]string) []int { + if rows.len == 0 { + return []int{} + } + + mut widths := []int{len: rows[0].len, init: 0} + for _, row in rows { + for i, cell in row { + if cell.len > widths[i] { + widths[i] = cell.len + } + } + } + return widths +} + +// format_row formats a single row with padding based on column widths +fn format_row(row []string, widths []int) string { + mut formatted_cells := []string{} + for i, cell in row { + formatted_cells << pad_right(cell, widths[i]) + } + return formatted_cells.join(' ') +} + +pub fn (mut s Sheet) pprint(args PPrintArgs) ! { + mut all_rows := [][]string{} + + // Prepare header row + mut header_row := ['Name'] + if args.description { + header_row << 'Description' + } + if args.aggrtype { + header_row << 'AggregateType' + } + if args.tags { + header_row << 'Tags' + } + if args.subgroup { + header_row << 'Subgroup' + } + + header_row << s.header()! + all_rows << header_row + + // Prepare data rows + for _, row in s.rows { + mut row_data := []string{} // Initialize row_data for each row + row_data << row.name // Add the name of the row + if args.description { + row_data << row.description + } + if args.aggrtype { + row_data << row.aggregatetype.str() + } + if args.tags { + row_data << row.tags + } + if args.subgroup { + row_data << row.subgroup + } + + for cell in row.cells { + if cell.empty { + row_data << '-' + } else { + row_data << float_repr(cell.val, row.reprtype) + } + } + mut is_empty_row := true + mut data_start_index := 1 // for row.name + if args.description { + data_start_index++ + } + if args.aggrtype { + data_start_index++ + } + if args.tags { + data_start_index++ + } + if args.subgroup { + data_start_index++ + } + + for i := data_start_index; i < row_data.len; i++ { + cell_val := row_data[i] + if cell_val.trim_space() != '' && cell_val.trim_space() != '-' { + is_empty_row = false + break + } + } + if !is_empty_row { + all_rows << row_data + } + } + + // Calculate column widths + widths := calculate_column_widths(all_rows) + + // Format all rows + mut formatted_result := []string{} + for _, row in all_rows { + formatted_result << '| ' + format_row(row, widths) + ' |' + } + + println(formatted_result.join('\n')) +}