diff --git a/playground/tables/CMakeLists.txt b/playground/tables/CMakeLists.txt index 5040f6f..ffc7535 100644 --- a/playground/tables/CMakeLists.txt +++ b/playground/tables/CMakeLists.txt @@ -1,6 +1,15 @@ -# Tables playground - vive dentro de primitives_gallery/ (playgrounds.md). -# No es un app del registry: no tiene app.md, no se indexa. +# Tables playground (cpp_apps.md / playgrounds.md). NO se indexa. add_imgui_app(tables_playground main.cpp - ${CMAKE_SOURCE_DIR}/functions/viz/table_view.cpp + data_table.cpp + data_table_logic.cpp +) + +# Self-test E2E (logica pura, sin ImGui). No depende de fn_framework. +add_executable(tables_playground_self_test + self_test.cpp + data_table_logic.cpp +) +target_include_directories(tables_playground_self_test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} ) diff --git a/playground/tables/data_table.cpp b/playground/tables/data_table.cpp new file mode 100644 index 0000000..0d0719d --- /dev/null +++ b/playground/tables/data_table.cpp @@ -0,0 +1,244 @@ +#include "data_table.h" +#include "imgui.h" + +#include +#include +#include + +namespace data_table { + +namespace { + +// Estado UI por-celda/por-header — sobrevive entre frames pero NO se persiste +// a disco. Si se promueve al registry hay que pasarlo al State del caller. +struct UiState { + int pending_col = -1; + std::string pending_value; + bool open_cell_popup = false; + + int header_popup_col = -1; + std::unordered_map filter_inputs; // col -> buffer + std::unordered_map color_value_inputs; // col -> buffer + std::unordered_map color_picker_vals; // col -> color +}; + +UiState& ui() { static UiState s; return s; } + +void ensure_init(State& st, int cols) { + if ((int)st.col_visible.size() != cols) st.col_visible.assign(cols, true); +} + +void draw_chips(State& st, const char* const* headers, int cols) { + if (st.filters.empty()) { + ImGui::TextDisabled("Sin filtros. Click en celda -> elige operador."); + return; + } + for (size_t i = 0; i < st.filters.size(); ) { + const auto& f = st.filters[i]; + const char* hdr = (f.col >= 0 && f.col < cols) ? headers[f.col] : "?"; + char buf[256]; + std::snprintf(buf, sizeof(buf), "%s %s %s x##chip%zu", + hdr, op_label(f.op), f.value.c_str(), i); + ImGui::PushStyleColor(ImGuiCol_Button, IM_COL32(60, 100, 160, 220)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, IM_COL32(80, 130, 200, 240)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, IM_COL32(45, 80, 130, 240)); + bool clicked = ImGui::SmallButton(buf); + ImGui::PopStyleColor(3); + if (clicked) { st.filters.erase(st.filters.begin() + i); continue; } + ImGui::SameLine(); + ++i; + } + ImGui::NewLine(); +} + +// Devuelve true y rellena out si el usuario eligio un operador. +bool draw_op_menu_items(Op& out) { + const Op ops[] = {Op::Eq, Op::Neq, Op::Gt, Op::Gte, Op::Lt, Op::Lte}; + for (Op o : ops) { + if (ImGui::MenuItem(op_label(o))) { out = o; return true; } + } + return false; +} + +void draw_header_menu(State& st, int col, const char* const* headers, int col_count) { + auto& U = ui(); + auto& fbuf = U.filter_inputs[col]; + fbuf.resize(256, '\0'); + + if (ImGui::BeginMenu("Filter...")) { + ImGui::SetNextItemWidth(180); + ImGui::InputText("##filterval", fbuf.data(), fbuf.size()); + std::string val(fbuf.c_str()); + const Op ops[] = {Op::Eq, Op::Neq, Op::Gt, Op::Gte, Op::Lt, Op::Lte}; + for (size_t i = 0; i < sizeof(ops)/sizeof(ops[0]); ++i) { + if (i > 0) ImGui::SameLine(); + if (ImGui::SmallButton(op_label(ops[i]))) { + st.filters.push_back({col, ops[i], val}); + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Conditional color")) { + auto& vbuf = U.color_value_inputs[col]; + vbuf.resize(256, '\0'); + auto it = U.color_picker_vals.find(col); + if (it == U.color_picker_vals.end()) { + U.color_picker_vals[col] = ImVec4(0.85f, 0.40f, 0.30f, 0.60f); + } + ImVec4& cv = U.color_picker_vals[col]; + ImGui::SetNextItemWidth(180); + ImGui::InputText("equals", vbuf.data(), vbuf.size()); + ImGui::ColorEdit4("color", &cv.x, ImGuiColorEditFlags_NoInputs); + if (ImGui::Button("Apply")) { + ImU32 c = ImGui::ColorConvertFloat4ToU32(cv); + st.color_rules.push_back({col, std::string(vbuf.c_str()), (unsigned int)c}); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Clear col")) { + for (size_t i = 0; i < st.color_rules.size();) { + if (st.color_rules[i].col == col) st.color_rules.erase(st.color_rules.begin() + i); + else ++i; + } + } + ImGui::EndMenu(); + } + + if (ImGui::MenuItem("Hide column")) { + st.col_visible[col] = false; + } + + ImGui::Separator(); + if (ImGui::BeginMenu("Columns")) { + for (int k = 0; k < col_count; ++k) { + bool v = st.col_visible[k]; + if (ImGui::Checkbox(headers[k], &v)) st.col_visible[k] = v; + } + if (ImGui::MenuItem("Show all")) { + for (int k = 0; k < col_count; ++k) st.col_visible[k] = true; + } + ImGui::EndMenu(); + } +} + +} // namespace + +void render(const char* id, + const char* const* headers, + int col_count, + const char* const* cells, + int row_count, + State& st) +{ + ensure_init(st, col_count); + auto& U = ui(); + + draw_chips(st, headers, col_count); + + auto visible_rows = compute_visible_rows(cells, row_count, col_count, st); + int visible_cols = 0; + for (bool v : st.col_visible) if (v) ++visible_cols; + + ImGui::Text("Filas: %d / %d Columnas: %d / %d", + (int)visible_rows.size(), row_count, visible_cols, col_count); + + if (visible_cols == 0) { + ImGui::TextDisabled("(todas las columnas ocultas - click derecho en cabecera anterior)"); + return; + } + + ImGuiTableFlags flags = + ImGuiTableFlags_Borders | + ImGuiTableFlags_Sortable | + ImGuiTableFlags_SortMulti | + ImGuiTableFlags_RowBg | + ImGuiTableFlags_Resizable | + ImGuiTableFlags_ScrollY | + ImGuiTableFlags_Reorderable; + + if (!ImGui::BeginTable(id, visible_cols, flags, ImVec2(0, 0))) return; + + // Setup columns with UserID = column index del dataset original. + for (int c = 0; c < col_count; ++c) { + if (!st.col_visible[c]) continue; + ImGui::TableSetupColumn(headers[c], ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)c); + } + ImGui::TableSetupScrollFreeze(0, 1); + + // Custom header row para soportar right-click context menu por columna. + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + int draw_col = 0; + for (int c = 0; c < col_count; ++c) { + if (!st.col_visible[c]) continue; + ImGui::TableSetColumnIndex(draw_col++); + ImGui::PushID(c); + ImGui::TableHeader(headers[c]); + if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { + U.header_popup_col = c; + ImGui::OpenPopup("##hdr_menu"); + } + if (ImGui::BeginPopup("##hdr_menu") && U.header_popup_col == c) { + draw_header_menu(st, c, headers, col_count); + ImGui::EndPopup(); + } + ImGui::PopID(); + } + + // Aplicar sort specs de ImGui -> State. + if (ImGuiTableSortSpecs* specs = ImGui::TableGetSortSpecs()) { + if (specs->SpecsDirty && specs->SpecsCount > 0) { + const ImGuiTableColumnSortSpecs& s = specs->Specs[0]; + st.sort_col = (int)s.ColumnUserID; + st.sort_desc = (s.SortDirection == ImGuiSortDirection_Descending); + specs->SpecsDirty = false; + visible_rows = compute_visible_rows(cells, row_count, col_count, st); + } else if (specs->SpecsCount == 0 && st.sort_col >= 0) { + st.sort_col = -1; + visible_rows = compute_visible_rows(cells, row_count, col_count, st); + } + } + + // Body. + for (int r : visible_rows) { + ImGui::TableNextRow(); + int dc = 0; + for (int c = 0; c < col_count; ++c) { + if (!st.col_visible[c]) continue; + ImGui::TableSetColumnIndex(dc++); + const char* cell = cells[r * col_count + c]; + for (const auto& cr : st.color_rules) { + if (cr.col == c && cell && cr.equals == cell) { + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, (ImU32)cr.color); + break; + } + } + ImGui::PushID(r * col_count + c); + if (ImGui::Selectable(cell ? cell : "", false, ImGuiSelectableFlags_AllowDoubleClick)) { + U.pending_col = c; + U.pending_value = cell ? cell : ""; + U.open_cell_popup = true; + } + ImGui::PopID(); + } + } + + ImGui::EndTable(); + + if (U.open_cell_popup) { ImGui::OpenPopup("##cell_op"); U.open_cell_popup = false; } + if (ImGui::BeginPopup("##cell_op")) { + const char* hdr = (U.pending_col >= 0 && U.pending_col < col_count) + ? headers[U.pending_col] : "?"; + ImGui::TextDisabled("%s ?? \"%s\"", hdr, U.pending_value.c_str()); + ImGui::Separator(); + Op picked; + if (draw_op_menu_items(picked)) { + st.filters.push_back({U.pending_col, picked, U.pending_value}); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +} // namespace data_table diff --git a/playground/tables/data_table.h b/playground/tables/data_table.h new file mode 100644 index 0000000..77a64ef --- /dev/null +++ b/playground/tables/data_table.h @@ -0,0 +1,16 @@ +#pragma once + +#include "data_table_logic.h" + +namespace data_table { + +// Render barra-de-chips + tabla. Mutates `st` en respuesta a interaccion. +// Caller mantiene el State entre frames. +void render(const char* id, + const char* const* headers, + int col_count, + const char* const* cells, + int row_count, + State& st); + +} // namespace data_table diff --git a/playground/tables/data_table_logic.cpp b/playground/tables/data_table_logic.cpp new file mode 100644 index 0000000..ad7bd14 --- /dev/null +++ b/playground/tables/data_table_logic.cpp @@ -0,0 +1,93 @@ +#include "data_table_logic.h" + +#include +#include +#include + +namespace data_table { + +const char* op_label(Op o) { + switch (o) { + case Op::Eq: return "="; + case Op::Neq: return "!="; + case Op::Gt: return ">"; + case Op::Gte: return ">="; + case Op::Lt: return "<"; + case Op::Lte: return "<="; + } + return "?"; +} + +bool parse_number(const char* s, double& out) { + if (!s || !*s) return false; + char* end = nullptr; + double v = std::strtod(s, &end); + if (end == s) return false; + while (*end == ' ' || *end == '\t') end++; + if (*end != '\0') return false; + out = v; + return true; +} + +bool compare(const char* a, const char* b, Op op) { + if (!a) a = ""; + if (!b) b = ""; + double na, nb; + bool numeric = parse_number(a, na) && parse_number(b, nb); + if (numeric) { + switch (op) { + case Op::Eq: return na == nb; + case Op::Neq: return na != nb; + case Op::Gt: return na > nb; + case Op::Gte: return na >= nb; + case Op::Lt: return na < nb; + case Op::Lte: return na <= nb; + } + } + int c = std::strcmp(a, b); + switch (op) { + case Op::Eq: return c == 0; + case Op::Neq: return c != 0; + case Op::Gt: return c > 0; + case Op::Gte: return c >= 0; + case Op::Lt: return c < 0; + case Op::Lte: return c <= 0; + } + return false; +} + +std::vector compute_visible_rows(const char* const* cells, + int rows, int cols, + const State& st) +{ + std::vector out; + out.reserve(rows); + for (int r = 0; r < rows; ++r) { + bool keep = true; + for (const auto& f : st.filters) { + if (f.col < 0 || f.col >= cols) continue; + const char* cell = cells[r * cols + f.col]; + if (!compare(cell, f.value.c_str(), f.op)) { keep = false; break; } + } + if (keep) out.push_back(r); + } + if (st.sort_col >= 0 && st.sort_col < cols) { + int sc = st.sort_col; + bool desc = st.sort_desc; + std::sort(out.begin(), out.end(), [&](int a, int b) { + const char* ca = cells[a * cols + sc]; + const char* cb = cells[b * cols + sc]; + if (!ca) ca = ""; + if (!cb) cb = ""; + double na, nb; + bool num = parse_number(ca, na) && parse_number(cb, nb); + int cmp; + if (num) cmp = (na < nb) ? -1 : (na > nb ? 1 : 0); + else cmp = std::strcmp(ca, cb); + return desc ? (cmp > 0) : (cmp < 0); + }); + } + return out; +} + +} // namespace data_table diff --git a/playground/tables/data_table_logic.h b/playground/tables/data_table_logic.h new file mode 100644 index 0000000..91d7aab --- /dev/null +++ b/playground/tables/data_table_logic.h @@ -0,0 +1,44 @@ +// Logica pura del playground data_table. Sin ImGui — testable headless. +// Cuando se promueva al registry, esto sera la base de data_table_cpp_viz. +#pragma once + +#include +#include + +namespace data_table { + +enum class Op { Eq, Neq, Gt, Gte, Lt, Lte }; +const char* op_label(Op o); + +struct Filter { + int col; + Op op; + std::string value; +}; + +struct ColorRule { + int col; + std::string equals; + unsigned int color; // ImU32 (ABGR para ImGui) +}; + +struct State { + std::vector filters; + std::vector color_rules; + std::vector col_visible; // size = col_count; auto-init en render + int sort_col = -1; // -1 = sin sort + bool sort_desc = false; +}; + +// Parse "1.23" -> 1.23, true. False si la celda no es numero completo. +bool parse_number(const char* s, double& out); + +// Compara dos celdas con operador. Numerico si ambas parseables; lexical si no. +bool compare(const char* a, const char* b, Op op); + +// Aplica filtros y ordena. Devuelve indices de filas visibles. +std::vector compute_visible_rows(const char* const* cells, + int rows, int cols, + const State& st); + +} // namespace data_table diff --git a/playground/tables/e2e_run.sh b/playground/tables/e2e_run.sh new file mode 100644 index 0000000..3e264b5 --- /dev/null +++ b/playground/tables/e2e_run.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# E2E playground tables. Compila + corre self-test linux + windows (si +# mingw esta disponible). Sale 0 si todo pasa. +set -euo pipefail + +ROOT="${FN_REGISTRY_ROOT:-$(cd "$(dirname "$0")/../../../../.." && pwd)}" +cd "$ROOT" + +echo "[e2e] linux build + self_test" +cmake -B cpp/build -S cpp >/dev/null +cmake --build cpp/build --target tables_playground_self_test -j"$(nproc)" >/dev/null +./cpp/build/apps/primitives_gallery/playground/tables/tables_playground_self_test + +if command -v x86_64-w64-mingw32-g++ >/dev/null 2>&1; then + echo "[e2e] windows cross-build (mingw)" + source "$ROOT/bash/functions/infra/build_cpp_windows.sh" + build_cpp_windows tables_playground_self_test >/dev/null + echo "[e2e] windows self_test via wine si disponible" + EXE="$ROOT/cpp/build/windows/apps/primitives_gallery/playground/tables/tables_playground_self_test.exe" + if [ -f "$EXE" ]; then + if command -v wine >/dev/null 2>&1; then + wine "$EXE" || { echo "[e2e] FAIL windows self_test (wine)"; exit 1; } + else + echo "[e2e] SKIP wine no instalado; binario en $EXE" + fi + fi +fi + +echo "[e2e] OK" diff --git a/playground/tables/main.cpp b/playground/tables/main.cpp index 52123a8..9e8ace5 100644 --- a/playground/tables/main.cpp +++ b/playground/tables/main.cpp @@ -1,14 +1,11 @@ -// Playground tables: visor de la funcion table_view_cpp_viz tal cual existe -// hoy en el registry. Iteraremos mejoras encima hasta promover una API v2 -// que sustituya a los `ImGui::BeginTable` raw de las apps C++. +// Playground tables: iterador de la fn `data_table` antes de promoverla al +// registry y migrar las apps C++ que hoy usan `ImGui::BeginTable` raw. #include "app_base.h" #include "imgui.h" -#include "viz/table_view.h" #include "core/logger.h" +#include "data_table.h" -#include -#include #include namespace { @@ -21,48 +18,44 @@ struct Row { const char* description; }; -// Dataset de muestra inspirado en el registry. Filas reales-ish para -// hacer obvias las limitaciones actuales (sin sort, sin filter, sin -// per-cell render, alto fijo, etc.). const std::vector& sample_rows() { static const std::vector rows = { - {"filter_slice", "go", "core", "pure", "Filtra slice con predicado"}, - {"map_slice", "go", "core", "pure", "Aplica f a cada elemento"}, - {"reduce_slice", "go", "core", "pure", "Fold con acumulador"}, - {"sma", "py", "finance", "pure", "Simple moving average"}, - {"ema", "py", "finance", "pure", "Exponential moving average"}, - {"rsi", "py", "finance", "pure", "Relative strength index"}, - {"table_view", "cpp", "viz", "pure", "Tabla ImGui actual del registry"}, - {"line_plot", "cpp", "viz", "pure", "ImPlot line wrapper"}, - {"scatter_plot", "cpp", "viz", "pure", "ImPlot scatter wrapper"}, - {"bar_chart", "cpp", "viz", "pure", "ImPlot bar wrapper"}, - {"heatmap", "cpp", "viz", "pure", "ImPlot heatmap wrapper"}, - {"sqlite_open", "go", "infra", "impure", "Open SQLite con WAL+FK"}, - {"http_json_response", "go", "infra", "impure", "Helper JSON response"}, - {"http_parse_body", "go", "infra", "impure", "Parse JSON body"}, - {"rsync_deploy", "bash", "infra", "impure", "rsync local -> remoto"}, - {"systemd_install", "go", "infra", "impure", "Sube unit + enable + start"}, - {"systemd_restart", "go", "infra", "impure", "Restart servicio remoto"}, - {"jupyter_discover", "py", "notebook", "impure", "Descubre instancias Jupyter"}, - {"jupyter_exec", "py", "notebook", "impure", "Ejecuta celda y vuelca output"}, - {"docker_pull_image", "go", "infra", "impure", "docker pull con timeout"}, - {"graph_force_layout", "cpp", "viz", "pure", "Force-directed CPU"}, - {"graph_force_layout_gpu","cpp", "viz", "pure", "Force-directed GPU (compute)"}, - {"sql_workbench", "cpp", "core", "impure", "Workbench SQL embebido"}, - {"text_editor", "cpp", "core", "impure", "Editor de texto con highlighting"}, - {"icon_font", "cpp", "core", "impure", "Carga tabler-icons.ttf"}, + {"filter_slice", "go", "core", "pure", "Filtra slice con predicado"}, + {"map_slice", "go", "core", "pure", "Aplica f a cada elemento"}, + {"reduce_slice", "go", "core", "pure", "Fold con acumulador"}, + {"sma", "py", "finance", "pure", "Simple moving average"}, + {"ema", "py", "finance", "pure", "Exponential moving average"}, + {"rsi", "py", "finance", "pure", "Relative strength index"}, + {"table_view", "cpp", "viz", "pure", "Tabla ImGui actual del registry"}, + {"line_plot", "cpp", "viz", "pure", "ImPlot line wrapper"}, + {"scatter_plot", "cpp", "viz", "pure", "ImPlot scatter wrapper"}, + {"bar_chart", "cpp", "viz", "pure", "ImPlot bar wrapper"}, + {"heatmap", "cpp", "viz", "pure", "ImPlot heatmap wrapper"}, + {"sqlite_open", "go", "infra", "impure", "Open SQLite con WAL+FK"}, + {"http_json_response", "go", "infra", "impure", "Helper JSON response"}, + {"http_parse_body", "go", "infra", "impure", "Parse JSON body"}, + {"rsync_deploy", "bash", "infra", "impure", "rsync local -> remoto"}, + {"systemd_install", "go", "infra", "impure", "Sube unit + enable + start"}, + {"systemd_restart", "go", "infra", "impure", "Restart servicio remoto"}, + {"jupyter_discover", "py", "notebook", "impure", "Descubre instancias Jupyter"}, + {"jupyter_exec", "py", "notebook", "impure", "Ejecuta celda y vuelca output"}, + {"docker_pull_image", "go", "infra", "impure", "docker pull con timeout"}, + {"graph_force_layout", "cpp", "viz", "pure", "Force-directed CPU"}, + {"graph_force_layout_gpu","cpp", "viz", "pure", "Force-directed GPU (compute)"}, + {"sql_workbench", "cpp", "core", "impure", "Workbench SQL embebido"}, + {"text_editor", "cpp", "core", "impure", "Editor de texto con highlighting"}, + {"icon_font", "cpp", "core", "impure", "Carga tabler-icons.ttf"}, }; return rows; } -// Aplanado row-major para alimentar table_view_cpp_viz (firma `const char* const*`). -const char* const* flatten_cells(int& out_rows, int& out_cols) { +const char* const* flatten_cells(int& rows, int& cols) { static std::vector flat; static bool built = false; if (!built) { - const auto& rows = sample_rows(); - flat.reserve(rows.size() * 5); - for (const auto& r : rows) { + const auto& src = sample_rows(); + flat.reserve(src.size() * 5); + for (const auto& r : src) { flat.push_back(r.name); flat.push_back(r.lang); flat.push_back(r.domain); @@ -71,30 +64,26 @@ const char* const* flatten_cells(int& out_rows, int& out_cols) { } built = true; } - out_rows = static_cast(sample_rows().size()); - out_cols = 5; + rows = (int)sample_rows().size(); + cols = 5; return flat.data(); } } // namespace void render() { - if (ImGui::Begin("Tables Playground - table_view actual")) { + static data_table::State st; + if (ImGui::Begin("Tables Playground - data_table v0.1")) { ImGui::TextWrapped( - "Esta es la funcion `table_view_cpp_viz` del registry hoy. " - "Capacidades: borders, sortable (solo indicador, no sort real), " - "rowBg, resizable, scrollY (alto fijo 300px), reorderable. " - "Sin filter, sin selection, sin per-cell render, sin export. " - "Iteraremos mejoras encima de esto."); + "Iteracion 1: sort real al pulsar header, click en celda -> popup operador " + "(=, !=, >, >=, <, <=) -> chip removible. Click derecho header: filter input, " + "conditional color, hide column, show/hide columns."); ImGui::Separator(); static const char* headers[] = {"name", "lang", "domain", "purity", "description"}; int rows = 0, cols = 0; const char* const* cells = flatten_cells(rows, cols); - - ImGui::Text("Filas: %d Columnas: %d", rows, cols); - ImGui::Spacing(); - table_view("##registry_sample", headers, cols, cells, rows); + data_table::render("##registry_sample", headers, cols, cells, rows, st); } ImGui::End(); } @@ -106,8 +95,8 @@ int main() { .width = 1280, .height = 800, .about = {.name = "tables_playground", - .version = "0.1.0", - .description = "Playground para iterar mejoras sobre table_view_cpp_viz antes de promover a registry y migrar apps C++."}, + .version = "0.2.0", + .description = "Playground para iterar mejoras sobre table_view antes de promover al registry."}, .log = {.file_path = "tables_playground.log", .level = static_cast(fn_log::Level::Info)} }, render); diff --git a/playground/tables/self_test.cpp b/playground/tables/self_test.cpp new file mode 100644 index 0000000..0b8ffc5 --- /dev/null +++ b/playground/tables/self_test.cpp @@ -0,0 +1,116 @@ +// E2E self-test del playground tables. Ejercita la logica pura +// (data_table_logic) sin ImGui. Build target separado: +// +// tables_playground_self_test -> linux +// tables_playground_self_test.exe -> windows +// +// Exit 0 = todos los checks pasan, 1 = falla. + +#include "data_table_logic.h" + +#include +#include +#include +#include + +namespace { + +int failed = 0; +int passed = 0; + +void check(bool cond, const char* name) { + if (cond) { passed++; std::printf("PASS %s\n", name); } + else { failed++; std::printf("FAIL %s\n", name); } +} + +} // namespace + +using namespace data_table; + +int main() { + // --- parse_number --- + double v = 0; + check(parse_number("1.23", v) && v == 1.23, "parse_number 1.23"); + check(parse_number("42", v) && v == 42.0, "parse_number 42"); + check(parse_number("-7.5", v) && v == -7.5, "parse_number -7.5"); + check(!parse_number("abc", v), "parse_number abc rejected"); + check(!parse_number("12x", v), "parse_number 12x rejected"); + check(!parse_number("", v), "parse_number empty rejected"); + check(!parse_number(nullptr, v), "parse_number null rejected"); + + // --- compare numerico --- + check( compare("10", "2", Op::Gt), "10 > 2 numerico"); + check(!compare("10", "2", Op::Lt), "10 < 2 numerico false"); + check( compare("2", "10", Op::Lt), "2 < 10 numerico"); + check( compare("5", "5", Op::Eq), "5 == 5 numerico"); + check( compare("5", "5", Op::Gte), "5 >= 5 numerico"); + check( compare("5", "5", Op::Lte), "5 <= 5 numerico"); + check( compare("5", "5", Op::Neq) == false, "5 != 5 numerico false"); + + // --- compare lexical (cuando no son numeros) --- + check( compare("go", "go", Op::Eq), "lexical eq"); + check( compare("go", "py", Op::Neq), "lexical neq"); + check( compare("py", "go", Op::Gt), "lexical gt"); + check( compare("ab", "ac", Op::Lt), "lexical lt"); + + // --- compute_visible_rows: filter --- + const char* cells[] = { + "a","1", + "b","2", + "c","3", + "a","4", + }; + State st; + st.filters.push_back({0, Op::Eq, "a"}); + auto rows = compute_visible_rows(cells, 4, 2, st); + check(rows.size() == 2 && rows[0] == 0 && rows[1] == 3, "filter col0 = a"); + + // --- filter numerico --- + st.filters.clear(); + st.filters.push_back({1, Op::Gt, "2"}); + rows = compute_visible_rows(cells, 4, 2, st); + check(rows.size() == 2 && rows[0] == 2 && rows[1] == 3, "filter col1 > 2"); + + // --- combinacion: > 1 AND col0 != b --- + st.filters.clear(); + st.filters.push_back({1, Op::Gt, "1"}); + st.filters.push_back({0, Op::Neq, "b"}); + rows = compute_visible_rows(cells, 4, 2, st); + check(rows.size() == 2 && rows[0] == 2 && rows[1] == 3, "filter combinado AND"); + + // --- sort ascendente numerico --- + st.filters.clear(); + st.sort_col = 1; + st.sort_desc = false; + rows = compute_visible_rows(cells, 4, 2, st); + check(rows.size() == 4 && rows[0] == 0 && rows[3] == 3, "sort asc numerico"); + + // --- sort descendente numerico --- + st.sort_desc = true; + rows = compute_visible_rows(cells, 4, 2, st); + check(rows.size() == 4 && rows[0] == 3 && rows[3] == 0, "sort desc numerico"); + + // --- sort lexical --- + st.sort_col = 0; + st.sort_desc = false; + rows = compute_visible_rows(cells, 4, 2, st); + check(rows.size() == 4 && std::strcmp(cells[rows[0]*2], "a") == 0 + && std::strcmp(cells[rows[3]*2], "c") == 0, "sort asc lexical"); + + // --- filter + sort combinado --- + st.sort_col = 1; + st.sort_desc = true; + st.filters.push_back({0, Op::Eq, "a"}); + rows = compute_visible_rows(cells, 4, 2, st); + check(rows.size() == 2 && rows[0] == 3 && rows[1] == 0, "filter+sort combinado"); + + // --- filter sobre columna inexistente: se ignora --- + st.filters.clear(); + st.filters.push_back({99, Op::Eq, "x"}); + st.sort_col = -1; + rows = compute_visible_rows(cells, 4, 2, st); + check(rows.size() == 4, "filter col fuera de rango ignorado"); + + std::printf("\n=== %d passed, %d failed ===\n", passed, failed); + return failed == 0 ? 0 : 1; +}