summaryrefslogtreecommitdiffstats
path: root/src/libixion/general_test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/libixion/general_test.cpp')
-rw-r--r--src/libixion/general_test.cpp1495
1 files changed, 1495 insertions, 0 deletions
diff --git a/src/libixion/general_test.cpp b/src/libixion/general_test.cpp
new file mode 100644
index 0000000..4409205
--- /dev/null
+++ b/src/libixion/general_test.cpp
@@ -0,0 +1,1495 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include "test_global.hpp" // This must be the first header to be included.
+
+#include <ixion/formula_name_resolver.hpp>
+#include <ixion/address.hpp>
+#include <ixion/formula.hpp>
+#include <ixion/model_context.hpp>
+#include <ixion/model_iterator.hpp>
+#include <ixion/named_expressions_iterator.hpp>
+#include <ixion/global.hpp>
+#include <ixion/macros.hpp>
+#include <ixion/interface/table_handler.hpp>
+#include <ixion/config.hpp>
+#include <ixion/matrix.hpp>
+#include <ixion/cell.hpp>
+#include <ixion/cell_access.hpp>
+#include <ixion/formula_result.hpp>
+#include <ixion/exceptions.hpp>
+
+#include <string>
+#include <cstring>
+#include <sstream>
+#include <thread>
+
+using namespace std;
+using namespace ixion;
+
+namespace {
+
+void test_size()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ cout << "test size" << endl;
+ cout << "* int: " << sizeof(int) << endl;
+ cout << "* long: " << sizeof(long) << endl;
+ cout << "* double: " << sizeof(double) << endl;
+ cout << "* size_t: " << sizeof(size_t) << endl;
+ cout << "* string_id_t: " << sizeof(string_id_t)
+ << " (min:" << std::numeric_limits<string_id_t>::min()
+ << "; max:" << std::numeric_limits<string_id_t>::max() << ")" << endl;
+ cout << "* celltype_t: " << sizeof(celltype_t) << endl;
+ cout << "* formula_cell: " << sizeof(formula_cell) << endl;
+ cout << "* formula_tokens_t: " << sizeof(formula_tokens_t) << endl;
+}
+
+void test_string_to_double()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ struct { const char* s; double v; } tests[] = {
+ { "12", 12.0 },
+ { "0", 0.0 },
+ { "1.3", 1.3 },
+ { "1234.00983", 1234.00983 },
+ { "-123.3", -123.3 }
+ };
+
+ size_t n = sizeof(tests) / sizeof(tests[0]);
+ for (size_t i = 0; i < n; ++i)
+ {
+ double v = to_double(tests[i].s);
+ assert(v == tests[i].v);
+ }
+}
+
+void test_string_pool()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ model_context cxt;
+
+ string_id_t s_table1 = cxt.append_string("Table1");
+ string_id_t s_table2 = cxt.append_string("Table2");
+ string_id_t s_cat = cxt.append_string("Category");
+ string_id_t s_val = cxt.append_string("Value");
+
+ cxt.dump_strings();
+
+ // Make sure these work correctly before proceeding further with the test.
+ assert(s_table1 == cxt.get_identifier_from_string("Table1"));
+ assert(s_table2 == cxt.get_identifier_from_string("Table2"));
+ assert(s_cat == cxt.get_identifier_from_string("Category"));
+ assert(s_val == cxt.get_identifier_from_string("Value"));
+}
+
+void test_formula_tokens_store()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ formula_tokens_store_ptr_t p = formula_tokens_store::create();
+ assert(p->get_reference_count() == 1);
+ auto p2 = p;
+
+ assert(p->get_reference_count() == 2);
+ assert(p2->get_reference_count() == 2);
+
+ auto p3(p);
+
+ assert(p->get_reference_count() == 3);
+ assert(p2->get_reference_count() == 3);
+ assert(p3->get_reference_count() == 3);
+
+ p3.reset();
+ assert(p->get_reference_count() == 2);
+ assert(p2->get_reference_count() == 2);
+
+ p2.reset();
+ assert(p->get_reference_count() == 1);
+ p.reset();
+}
+
+void test_matrix()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ struct check
+ {
+ size_t row;
+ size_t col;
+ double val;
+ };
+
+ std::vector<check> checks =
+ {
+ { 0, 0, 1.0 },
+ { 0, 1, 2.0 },
+ { 1, 0, 3.0 },
+ { 1, 1, 4.0 },
+ };
+
+ numeric_matrix num_mtx(2, 2);
+
+ for (const check& c : checks)
+ num_mtx(c.row, c.col) = c.val;
+
+ for (const check& c : checks)
+ assert(num_mtx(c.row, c.col) == c.val);
+
+ matrix mtx(num_mtx);
+
+ for (const check& c : checks)
+ {
+ matrix::element e = mtx.get(c.row, c.col);
+ assert(e.type == matrix::element_type::numeric);
+ assert(std::get<double>(e.value) == c.val);
+ }
+}
+
+void test_matrix_non_numeric_values()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ matrix mtx(2, 2);
+ mtx.set(0, 0, 1.1);
+ mtx.set(1, 0, formula_error_t::division_by_zero);
+ mtx.set(0, 1, std::string("foo"));
+ mtx.set(1, 1, true);
+
+ assert(mtx.get_numeric(0, 0) == 1.1);
+
+ matrix::element elem = mtx.get(1, 0);
+ assert(elem.type == matrix::element_type::error);
+ assert(std::get<formula_error_t>(elem.value) == formula_error_t::division_by_zero);
+
+ elem = mtx.get(0, 1);
+ assert(elem.type == matrix::element_type::string);
+ assert(std::get<std::string_view>(elem.value) == "foo");
+
+ elem = mtx.get(1, 1);
+ assert(elem.type == matrix::element_type::boolean);
+ assert(std::get<bool>(elem.value) == true);
+}
+
+void test_address()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ {
+ address_t addr(-1, 0, 0, false, false, false);
+ abs_address_t pos(1, 0, 0);
+ abs_address_t abs_addr = addr.to_abs(pos);
+ assert(abs_addr.sheet == 0 && abs_addr.row == 0 && abs_addr.column == 0);
+
+ abs_address_t pos_invalid_sheet(invalid_sheet, 2, 3);
+ auto test = addr.to_abs(pos_invalid_sheet);
+ assert(test.sheet == invalid_sheet);
+ assert(test.row == 2);
+ assert(test.column == 3);
+ }
+
+
+ // Default constructor makes valid address.
+ assert(abs_address_t().valid());
+ assert(abs_range_t().valid());
+
+ // These are invalid addresses.
+ assert(!abs_address_t(abs_address_t::invalid).valid());
+ assert(!abs_range_t(abs_range_t::invalid).valid());
+
+ {
+ abs_range_t range(1, 1, 2, 3, 3);
+ assert(range.first.sheet == 1);
+ assert(range.first.row == 1);
+ assert(range.first.column == 2);
+ assert(range.last.sheet == 1);
+ assert(range.last.row == 3);
+ assert(range.last.column == 4);
+
+ abs_range_t range2(range);
+ assert(range2 == range);
+
+ abs_rc_range_t rc_range(range);
+ assert(rc_range.first.row == 1);
+ assert(rc_range.first.column == 2);
+ assert(rc_range.last.row == 3);
+ assert(rc_range.last.column == 4);
+ }
+}
+
+bool check_formula_expression(
+ model_context& cxt, const formula_name_resolver& resolver, const char* p)
+{
+ size_t n = strlen(p);
+ cout << "testing formula expression '" << p << "'" << endl;
+
+ formula_tokens_t tokens = parse_formula_string(cxt, abs_address_t(), resolver, {p, n});
+ std::string expression = print_formula_tokens(cxt, abs_address_t(), resolver, tokens);
+
+ int res = strcmp(p, expression.data());
+ if (res)
+ {
+ cout << "formula expressions differ: '" << p << "' (before) -> '" << expression << "' (after)" << endl;
+ return false;
+ }
+
+ std::ostringstream os;
+ for (const auto& t : tokens)
+ os << print_formula_token(cxt, abs_address_t(), resolver, t);
+ std::string individual_tokens = os.str();
+
+ if (expression != individual_tokens)
+ {
+ cout << "whole expression differs from individual token strings:" << endl
+ << " * expression='" << expression << "'" << endl
+ << " * individual-tokens='" << individual_tokens << "'" << endl;
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Make sure the public API works as advertized.
+ */
+void test_parse_and_print_expressions()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ // Excel A1
+
+ std::vector<const char*> exps = {
+ "\" \"",
+ "1/3*1.4",
+ "2.3*(1+2)/(34*(3-2))",
+ "SUM(1,2,3)",
+ "A1",
+ "B10",
+ "XFD1048576",
+ "C10:D20",
+ "A1:XFD1048576",
+ "H:H",
+ "B:D",
+ "AB:AD",
+ "2:2",
+ "3:5",
+ "34:36",
+ "1>2",
+ "1>=2",
+ "1<2",
+ "1<=2",
+ "1<>2",
+ "1=2",
+ "Table1[Category]",
+ "Table1[Value]",
+ "Table1[#Headers]",
+ "Table1[[#Headers],[Category]:[Value]]",
+ "Table1[[#Headers],[#Data],[Category]:[Value]]",
+ "IF(A1=\"\",\"empty\",\"not empty\")",
+ "$'Ying & Yang'.$A$1:$H$54",
+ };
+
+ model_context cxt;
+ cxt.append_sheet("Test");
+ cxt.append_string("Table1");
+ cxt.append_string("Category");
+ cxt.append_string("Value");
+ cxt.append_sheet("Ying & Yang"); // name with '&'
+
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::excel_a1, &cxt);
+ assert(resolver);
+
+ for (const char* exp : exps)
+ {
+ bool result = check_formula_expression(cxt, *resolver, exp);
+ assert(result);
+ }
+
+ // Excel R1C1
+
+ exps = {
+ "SUM(R[-5]C:R[-1]C)",
+ };
+
+ resolver = formula_name_resolver::get(formula_name_resolver_t::excel_r1c1, &cxt);
+ assert(resolver);
+
+ for (const char* exp : exps)
+ {
+ bool result = check_formula_expression(cxt, *resolver, exp);
+ assert(result);
+ }
+
+ // ODFF
+
+ exps = {
+ "\" \"",
+ "SUM([.A1];[.B1])",
+ "CONCATENATE([.A6];\" \";[.B6])",
+ "IF(['Ying & Yang'.$A$1:.$O$200];2;0)",
+ };
+
+ resolver = formula_name_resolver::get(formula_name_resolver_t::odff, &cxt);
+ assert(resolver);
+
+ config cfg = cxt.get_config();
+ cfg.sep_function_arg = ';';
+ cxt.set_config(cfg);
+
+ for (const char* exp : exps)
+ {
+ bool result = check_formula_expression(cxt, *resolver, exp);
+ assert(result);
+ }
+}
+
+/**
+ * Function name must be resolved case-insensitively.
+ */
+void test_function_name_resolution()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ const char* valid_names[] = {
+ "SUM", "sum", "Sum", "Average", "max", "min"
+ };
+
+ const char* invalid_names[] = {
+ "suma", "foo", "", "su", "maxx", "minmin"
+ };
+
+ model_context cxt;
+ cxt.append_sheet("Test");
+ auto resolver = formula_name_resolver::get(ixion::formula_name_resolver_t::excel_a1, &cxt);
+ size_t n = std::size(valid_names);
+ for (size_t i = 0; i < n; ++i)
+ {
+ const char* name = valid_names[i];
+ cout << "valid name: " << name << endl;
+ formula_name_t t = resolver->resolve(name, abs_address_t());
+ assert(t.type == formula_name_t::function);
+ }
+
+ n = std::size(invalid_names);
+ for (size_t i = 0; i < n; ++i)
+ {
+ const char* name = invalid_names[i];
+ cout << "invalid name: " << name << endl;
+ formula_name_t t = resolver->resolve(name, abs_address_t());
+ assert(t.type != formula_name_t::function);
+ }
+}
+
+formula_cell* insert_formula(
+ model_context& cxt, const abs_address_t& pos, const char* exp,
+ const formula_name_resolver& resolver)
+{
+ formula_tokens_t tokens = parse_formula_string(cxt, pos, resolver, exp);
+ auto ts = formula_tokens_store::create();
+ ts->get() = std::move(tokens);
+ formula_cell* p_inserted = cxt.set_formula_cell(pos, ts);
+ assert(p_inserted);
+ register_formula_cell(cxt, pos);
+ formula_cell* p = cxt.get_formula_cell(pos);
+ assert(p);
+ assert(p == p_inserted);
+ return p;
+}
+
+void test_model_context_storage()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ {
+ model_context cxt;
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::excel_a1, &cxt);
+ assert(resolver);
+
+ cxt.append_sheet("test");
+
+ // Test empty cell access.
+ cell_access ca = cxt.get_cell_access(abs_address_t(0, 0, 0));
+ assert(ca.get_type() == celltype_t::empty);
+ assert(ca.get_value_type() == cell_value_t::empty);
+
+ // String value on an empty cell should be an empty string.
+ std::string_view s = ca.get_string_value();
+ assert(s.empty());
+
+ // Likewise...
+ s = cxt.get_string_value(abs_address_t(0, 0, 0));
+ assert(s.empty());
+
+ // Test storage of numeric values.
+ volatile double val = 0.1;
+ for (col_t col = 0; col < 3; ++col)
+ {
+ for (row_t row = 0; row < 3; ++row)
+ {
+ abs_address_t pos(0, row, col);
+ cxt.set_numeric_cell(pos, val);
+ double test = cxt.get_numeric_value(pos);
+ assert(test == val);
+
+ ca = cxt.get_cell_access(pos);
+ assert(ca.get_type() == celltype_t::numeric);
+ assert(ca.get_value_type() == cell_value_t::numeric);
+ test = ca.get_numeric_value();
+ assert(test == val);
+
+ val += 0.2;
+ }
+ }
+
+ // Test formula cells.
+ abs_address_t pos(0,3,0);
+ const char* exp = "SUM(1,2,3)";
+ formula_tokens_t tokens = parse_formula_string(cxt, pos, *resolver, exp);
+ auto ts = formula_tokens_store::create();
+ ts->get() = std::move(tokens);
+ formula_cell* p_inserted = cxt.set_formula_cell(pos, ts);
+ assert(p_inserted);
+ formula_cell* p = cxt.get_formula_cell(pos);
+ assert(p);
+ assert(p_inserted == p);
+ p->interpret(cxt, pos);
+
+ ca = cxt.get_cell_access(pos);
+ assert(ca.get_type() == celltype_t::formula);
+ assert(ca.get_value_type() == cell_value_t::numeric);
+ assert(ca.get_numeric_value() == 6.0);
+ }
+
+ {
+ model_context cxt;
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::excel_a1, &cxt);
+ assert(resolver);
+
+ cxt.append_sheet("test");
+ string exp = "1";
+ cxt.set_formula_cell(abs_address_t(0,0,0), parse_formula_string(cxt, abs_address_t(0,0,0), *resolver, exp));
+ cxt.set_formula_cell(abs_address_t(0,2,0), parse_formula_string(cxt, abs_address_t(0,2,0), *resolver, exp));
+ cxt.set_formula_cell(abs_address_t(0,1,0), parse_formula_string(cxt, abs_address_t(0,1,0), *resolver, exp));
+ }
+
+ {
+ // Test data area.
+ model_context cxt;
+ cxt.append_sheet("test");
+
+ abs_range_t area = cxt.get_data_range(0);
+ assert(!area.valid());
+
+ cxt.set_numeric_cell(abs_address_t(0, 6, 5), 1.1);
+ area = cxt.get_data_range(0);
+ assert(area.first == area.last);
+ assert(area.first.sheet == 0);
+ assert(area.first.row == 6);
+ assert(area.first.column == 5);
+
+ cxt.set_numeric_cell(abs_address_t(0, 2, 3), 1.1);
+ area = cxt.get_data_range(0);
+ assert(area.first.sheet == 0);
+ assert(area.first.row == 2);
+ assert(area.first.column == 3);
+ assert(area.last.sheet == 0);
+ assert(area.last.row == 6);
+ assert(area.last.column == 5);
+
+ cxt.set_numeric_cell(abs_address_t(0, 7, 1), 1.1);
+ area = cxt.get_data_range(0);
+ assert(area.first.sheet == 0);
+ assert(area.first.row == 2);
+ assert(area.first.column == 1);
+ assert(area.last.sheet == 0);
+ assert(area.last.row == 7);
+ assert(area.last.column == 5);
+
+ // This shouldn't change the data range.
+ cxt.set_numeric_cell(abs_address_t(0, 5, 5), 1.1);
+ abs_range_t test = cxt.get_data_range(0);
+ assert(test == area);
+ }
+
+ {
+ // Fill up the document model and make sure the data range is still
+ // correct.
+ const row_t row_size = 5;
+ const col_t col_size = 4;
+ model_context cxt({row_size, col_size});
+ cxt.append_sheet("test");
+ for (row_t row = 0; row < row_size; ++row)
+ for (col_t col = 0; col < col_size; ++col)
+ cxt.set_numeric_cell(abs_address_t(0,row,col), 1.0);
+
+ abs_range_t test = cxt.get_data_range(0);
+
+ assert(test.first.sheet == 0);
+ assert(test.first.row == 0);
+ assert(test.first.column == 0);
+ assert(test.last.sheet == 0);
+ assert(test.last.row == row_size-1);
+ assert(test.last.column == col_size-1);
+ }
+
+ {
+ const row_t row_size = 5;
+ const col_t col_size = 4;
+ model_context cxt({row_size, col_size});
+ cxt.append_sheet("test");
+ cxt.set_numeric_cell(abs_address_t(0,0,0), 1.0);
+ cxt.set_numeric_cell(abs_address_t(0,row_size-1,0), 1.0);
+ cxt.set_numeric_cell(abs_address_t(0,row_size/2,col_size/2), 1.0);
+
+ abs_range_t test = cxt.get_data_range(0);
+
+ assert(test.first.sheet == 0);
+ assert(test.first.row == 0);
+ assert(test.first.column == 0);
+ assert(test.last.sheet == 0);
+ assert(test.last.row == row_size-1);
+ assert(test.last.column == col_size/2);
+ }
+}
+
+void test_model_context_direct_string_access()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ model_context cxt{{400, 20}};
+ cxt.append_sheet("test");
+
+ // regular string cell
+ abs_address_t B2(0, 1, 1);
+ cxt.set_string_cell(B2, "string cell");
+ std::string_view s = cxt.get_string_value(B2);
+ assert(s == "string cell");
+
+ cell_access ca = cxt.get_cell_access(B2);
+ assert(ca.get_type() == celltype_t::string);
+ assert(ca.get_value_type() == cell_value_t::string);
+ s = ca.get_string_value();
+ assert(s == "string cell");
+
+ // formula cell containing a string result.
+ abs_address_t C4(0, 3, 2);
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::calc_a1, &cxt);
+ assert(resolver);
+
+ // Insert a formula containing one literal string token.
+ formula_tokens_t tokens = parse_formula_string(cxt, C4, *resolver, "\"string value in formula\"");
+ assert(tokens.size() == 1);
+ cxt.set_formula_cell(C4, std::move(tokens));
+ // no need to register formula cell since it does not reference other cells.
+
+ abs_range_set_t formula_cells{C4};
+ auto sorted = query_and_sort_dirty_cells(cxt, abs_range_set_t(), &formula_cells);
+ calculate_sorted_cells(cxt, sorted, 1);
+
+ s = cxt.get_string_value(C4);
+ assert(s == "string value in formula");
+
+ ca = cxt.get_cell_access(C4);
+ assert(ca.get_type() == celltype_t::formula);
+ assert(ca.get_value_type() == cell_value_t::string);
+ s = ca.get_string_value();
+ assert(s == "string value in formula");
+}
+
+void test_model_context_named_expression()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ model_context cxt{{400, 20}};
+ cxt.append_sheet("test");
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::calc_a1, &cxt);
+ assert(resolver);
+
+ abs_address_t B3(0, 2, 1);
+
+ struct test_case
+ {
+ std::string name;
+ std::string formula;
+ abs_address_t origin;
+ };
+
+ std::vector<test_case> tcs = {
+ { "LeftAndAbove", "A3+B2", B3 },
+ { "SumAboveRow", "SUM(A2:D2)", B3 },
+ };
+
+ for (const test_case& tc : tcs)
+ {
+ formula_tokens_t tokens = parse_formula_string(cxt, tc.origin, *resolver, tc.formula);
+ std::string test = print_formula_tokens(cxt, tc.origin, *resolver, tokens);
+ assert(test == tc.formula);
+
+ cxt.set_named_expression(tc.name, tc.origin, std::move(tokens));
+ }
+
+ for (const test_case& tc : tcs)
+ {
+ const named_expression_t* exp = cxt.get_named_expression(0, tc.name);
+ assert(exp);
+ assert(exp->origin == tc.origin);
+ std::string test = print_formula_tokens(cxt, exp->origin, *resolver, exp->tokens);
+ assert(test == tc.formula);
+ }
+
+ // invalid names should be rejected.
+ struct name_test_case
+ {
+ std::string name;
+ bool valid;
+ };
+
+ std::vector<name_test_case> invalid_names = {
+ { "Name 1", false },
+ { "Name_1", true },
+ { "123Name", false },
+ { "Name123", true },
+ { "", false },
+ { "Name.1", true },
+ { ".Name.2", false },
+ };
+
+ for (const name_test_case& tc : invalid_names)
+ {
+ abs_address_t origin;
+ std::string formula = "1+2";
+
+ if (tc.valid)
+ {
+ formula_tokens_t tokens = parse_formula_string(cxt, origin, *resolver, formula);
+ cxt.set_named_expression(tc.name, origin, std::move(tokens));
+
+ tokens = parse_formula_string(cxt, origin, *resolver, formula);
+ cxt.set_named_expression(0, tc.name, origin, std::move(tokens));
+ }
+ else
+ {
+ try
+ {
+ formula_tokens_t tokens = parse_formula_string(cxt, origin, *resolver, formula);
+ cxt.set_named_expression(tc.name, origin, std::move(tokens));
+ assert(!"named expression with invalid name should have been rejected!");
+ }
+ catch (const model_context_error& e)
+ {
+ assert(e.get_error_type() == model_context_error::invalid_named_expression);
+ }
+
+ try
+ {
+ formula_tokens_t tokens = parse_formula_string(cxt, origin, *resolver, formula);
+ cxt.set_named_expression(0, tc.name, origin, std::move(tokens));
+ assert(!"named expression with invalid name should have been rejected!");
+ }
+ catch (const model_context_error& e)
+ {
+ assert(e.get_error_type() == model_context_error::invalid_named_expression);
+ }
+ }
+ }
+}
+
+bool check_model_iterator_output(model_iterator& iter, const std::vector<model_iterator::cell>& checks)
+{
+ for (const model_iterator::cell& c : checks)
+ {
+ if (!iter.has())
+ {
+ cerr << "a cell value was expected, but none found." << endl;
+ return false;
+ }
+
+ if (iter.get() != c)
+ {
+ cerr << "unexpected cell value: expected=" << c << "; observed=" << iter.get() << endl;
+ return false;
+ }
+
+ iter.next();
+ }
+
+ if (iter.has())
+ {
+ cerr << "an additional cell value was found, but none was expected." << endl;
+ return false;
+ }
+
+ return true;
+}
+
+void test_model_context_iterator_horizontal()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ const row_t row_size = 5;
+ const col_t col_size = 2;
+ model_context cxt{{row_size, col_size}};
+ model_iterator iter;
+
+ abs_rc_range_t whole_range;
+ whole_range.set_all_columns();
+ whole_range.set_all_rows();
+
+ // It should not crash or throw an exception on empty model.
+ iter = cxt.get_model_iterator(0, rc_direction_t::horizontal, whole_range);
+ assert(!iter.has());
+
+ // Insert an actual sheet and try again.
+
+ cxt.append_sheet("empty sheet");
+ iter = cxt.get_model_iterator(0, rc_direction_t::horizontal, whole_range);
+
+ // Make sure the cell position iterates correctly.
+ size_t cell_count = 0;
+ for (row_t row = 0; row < row_size; ++row)
+ {
+ for (col_t col = 0; col < col_size; ++cell_count, ++col, iter.next())
+ {
+ assert(iter.has());
+ assert(iter.get().row == row);
+ assert(iter.get().col == col);
+ assert(iter.get().type == celltype_t::empty);
+ }
+ }
+
+ assert(!iter.has()); // There should be no more cells on this sheet.
+ assert(cell_count == 10);
+
+ cxt.append_sheet("values");
+ cxt.set_string_cell(abs_address_t(1, 0, 0), "F1");
+ cxt.set_string_cell(abs_address_t(1, 0, 1), "F2");
+ cxt.set_boolean_cell(abs_address_t(1, 1, 0), true);
+ cxt.set_boolean_cell(abs_address_t(1, 1, 1), false);
+ cxt.set_numeric_cell(abs_address_t(1, 2, 0), 3.14);
+ cxt.set_numeric_cell(abs_address_t(1, 2, 1), -12.5);
+
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::excel_a1, &cxt);
+ abs_range_set_t modified_cells;
+ abs_address_t pos(1, 3, 0);
+ formula_tokens_t tokens = parse_formula_string(cxt, pos, *resolver, "SUM(1, 2, 3)");
+ formula_cell* p = cxt.set_formula_cell(pos, std::move(tokens));
+ assert(p);
+ const formula_tokens_t& t = p->get_tokens()->get();
+ assert(t.size() == 8); // there should be 8 tokens.
+ register_formula_cell(cxt, pos, p);
+ modified_cells.insert(pos);
+
+ pos.column = 1;
+ tokens = parse_formula_string(cxt, pos, *resolver, "5 + 6 - 7");
+ p = cxt.set_formula_cell(pos, std::move(tokens));
+ register_formula_cell(cxt, pos, p);
+ modified_cells.insert(pos);
+
+ // Calculate the formula cells.
+ auto sorted = query_and_sort_dirty_cells(cxt, abs_range_set_t(), &modified_cells);
+ calculate_sorted_cells(cxt, sorted, 1);
+
+ std::vector<model_iterator::cell> checks =
+ {
+ // row, column, value
+ { 0, 0, cxt.get_identifier_from_string("F1") },
+ { 0, 1, cxt.get_identifier_from_string("F2") },
+ { 1, 0, true },
+ { 1, 1, false },
+ { 2, 0, 3.14 },
+ { 2, 1, -12.5 },
+ { 3, 0, cxt.get_formula_cell(abs_address_t(1, 3, 0)) },
+ { 3, 1, cxt.get_formula_cell(abs_address_t(1, 3, 1)) },
+ { 4, 0 },
+ { 4, 1 },
+ };
+
+ // Iterator and check the individual cell values.
+ iter = cxt.get_model_iterator(1, rc_direction_t::horizontal, whole_range);
+ assert(check_model_iterator_output(iter, checks));
+}
+
+void test_model_context_iterator_horizontal_range()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ nullptr_t empty = nullptr;
+ model_context cxt{{10, 5}};
+ cxt.append_sheet("Values");
+ cxt.set_cell_values(0, {
+ { "F1", "F2", "F3", "F4", "F5" },
+ { 1.0, true, "s1", empty, empty },
+ { 1.1, false, empty, "s2", empty },
+ { 1.2, false, empty, "s3", empty },
+ { 1.3, true, empty, "s4", empty },
+ { 1.4, false, empty, "s5", empty },
+ { 1.5, "NA", empty, "s6", empty },
+ { 1.6, 99.9, empty, "s7", empty },
+ { 1.7, 199.9, empty, "s8", empty },
+ { 1.8, 299.9, empty, "s9", "end" },
+ });
+
+ // Only iterate over the first two rows.
+ abs_rc_range_t range;
+ range.set_all_columns();
+ range.first.row = 0;
+ range.last.row = 1;
+
+ model_iterator iter = cxt.get_model_iterator(0, rc_direction_t::horizontal, range);
+
+ std::vector<model_iterator::cell> checks =
+ {
+ // row, column, value
+ { 0, 0, cxt.get_identifier_from_string("F1") },
+ { 0, 1, cxt.get_identifier_from_string("F2") },
+ { 0, 2, cxt.get_identifier_from_string("F3") },
+ { 0, 3, cxt.get_identifier_from_string("F4") },
+ { 0, 4, cxt.get_identifier_from_string("F5") },
+ { 1, 0, 1.0 },
+ { 1, 1, true },
+ { 1, 2, cxt.get_identifier_from_string("s1") },
+ { 1, 3 },
+ { 1, 4 },
+ };
+
+ assert(check_model_iterator_output(iter, checks));
+
+ // Only iterate over rows 2:4.
+ range.first.row = 2;
+ range.last.row = 4;
+ iter = cxt.get_model_iterator(0, rc_direction_t::horizontal, range);
+
+ checks =
+ {
+ // row, column, value
+ { 2, 0, 1.1 },
+ { 2, 1, false },
+ { 2, 2 },
+ { 2, 3, cxt.get_identifier_from_string("s2") },
+ { 2, 4 },
+ { 3, 0, 1.2 },
+ { 3, 1, false },
+ { 3, 2 },
+ { 3, 3, cxt.get_identifier_from_string("s3") },
+ { 3, 4 },
+ { 4, 0, 1.3 },
+ { 4, 1, true },
+ { 4, 2 },
+ { 4, 3, cxt.get_identifier_from_string("s4") },
+ { 4, 4 },
+ };
+
+ assert(check_model_iterator_output(iter, checks));
+
+ // Only iterate over columns 1:3 and only down to row 4.
+ range.set_all_rows();
+ range.first.column = 1;
+ range.last.column = 3;
+ range.last.row = 4;
+ iter = cxt.get_model_iterator(0, rc_direction_t::horizontal, range);
+
+ checks =
+ {
+ // row, column, value
+ { 0, 1, cxt.get_identifier_from_string("F2") },
+ { 0, 2, cxt.get_identifier_from_string("F3") },
+ { 0, 3, cxt.get_identifier_from_string("F4") },
+ { 1, 1, true },
+ { 1, 2, cxt.get_identifier_from_string("s1") },
+ { 1, 3 },
+ { 2, 1, false },
+ { 2, 2 },
+ { 2, 3, cxt.get_identifier_from_string("s2") },
+ { 3, 1, false },
+ { 3, 2 },
+ { 3, 3, cxt.get_identifier_from_string("s3") },
+ { 4, 1, true },
+ { 4, 2 },
+ { 4, 3, cxt.get_identifier_from_string("s4") },
+ };
+
+ assert(check_model_iterator_output(iter, checks));
+}
+
+void test_model_context_iterator_vertical()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ const row_t row_size = 5;
+ const col_t col_size = 2;
+ model_context cxt{{row_size, col_size}};
+ model_iterator iter;
+
+ abs_rc_range_t whole_range;
+ whole_range.set_all_columns();
+ whole_range.set_all_rows();
+
+ // It should not crash or throw an exception on empty model.
+ iter = cxt.get_model_iterator(0, rc_direction_t::vertical, whole_range);
+ assert(!iter.has());
+
+ // Insert an actual sheet and try again.
+
+ cxt.append_sheet("empty sheet");
+ iter = cxt.get_model_iterator(0, rc_direction_t::vertical, whole_range);
+
+ // Make sure the cell position iterates correctly.
+ size_t cell_count = 0;
+ for (col_t col = 0; col < col_size; ++col)
+ {
+ for (row_t row = 0; row < row_size; ++cell_count, ++row, iter.next())
+ {
+ const model_iterator::cell& cell = iter.get();
+ assert(iter.has());
+ assert(cell.row == row);
+ assert(cell.col == col);
+ assert(cell.type == celltype_t::empty);
+ }
+ }
+
+ assert(!iter.has()); // There should be no more cells on this sheet.
+ assert(cell_count == 10);
+
+ cxt.append_sheet("values");
+ cxt.set_string_cell(abs_address_t(1, 0, 0), "F1");
+ cxt.set_string_cell(abs_address_t(1, 0, 1), "F2");
+ cxt.set_boolean_cell(abs_address_t(1, 1, 0), true);
+ cxt.set_boolean_cell(abs_address_t(1, 1, 1), false);
+ cxt.set_numeric_cell(abs_address_t(1, 2, 0), 3.14);
+ cxt.set_numeric_cell(abs_address_t(1, 2, 1), -12.5);
+
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::excel_a1, &cxt);
+ abs_range_set_t modified_cells;
+ abs_address_t pos(1, 3, 0);
+ formula_tokens_t tokens = parse_formula_string(cxt, pos, *resolver, "SUM(1, 2, 3)");
+ cxt.set_formula_cell(pos, std::move(tokens));
+ register_formula_cell(cxt, pos);
+ modified_cells.insert(pos);
+
+ pos.column = 1;
+ tokens = parse_formula_string(cxt, pos, *resolver, "5 + 6 - 7");
+ cxt.set_formula_cell(pos, std::move(tokens));
+ register_formula_cell(cxt, pos);
+ modified_cells.insert(pos);
+
+ // Calculate the formula cells.
+ auto sorted = query_and_sort_dirty_cells(cxt, abs_range_set_t(), &modified_cells);
+ calculate_sorted_cells(cxt, sorted, 1);
+
+ std::vector<model_iterator::cell> checks =
+ {
+ // row, column, value
+ { 0, 0, cxt.get_identifier_from_string("F1") },
+ { 1, 0, true },
+ { 2, 0, 3.14 },
+ { 3, 0, cxt.get_formula_cell(abs_address_t(1, 3, 0)) },
+ { 4, 0 },
+
+ { 0, 1, cxt.get_identifier_from_string("F2") },
+ { 1, 1, false },
+ { 2, 1, -12.5 },
+ { 3, 1, cxt.get_formula_cell(abs_address_t(1, 3, 1)) },
+ { 4, 1 },
+ };
+
+ iter = cxt.get_model_iterator(1, rc_direction_t::vertical, whole_range);
+ assert(check_model_iterator_output(iter, checks));
+}
+
+void test_model_context_iterator_vertical_range()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ nullptr_t empty = nullptr;
+ model_context cxt{{10, 5}};
+ cxt.append_sheet("Values");
+ cxt.set_cell_values(0, {
+ { "F1", "F2", "F3", "F4", "F5" },
+ { 1.0, true, "s1", empty, empty },
+ { 1.1, false, empty, "s2", empty },
+ { 1.2, false, empty, "s3", empty },
+ { 1.3, true, empty, "s4", empty },
+ { 1.4, false, empty, "s5", empty },
+ { 1.5, "NA", empty, "s6", empty },
+ { 1.6, 99.9, empty, "s7", empty },
+ { 1.7, 199.9, empty, "s8", empty },
+ { 1.8, 299.9, empty, "s9", "end" },
+ });
+
+ // Iterate over the top 2 rows.
+ abs_rc_range_t range;
+ range.set_all_columns();
+ range.set_all_rows();
+ range.last.row = 1;
+
+ model_iterator iter = cxt.get_model_iterator(0, rc_direction_t::vertical, range);
+
+ std::vector<model_iterator::cell> checks =
+ {
+ // row, column, value
+ { 0, 0, cxt.get_identifier_from_string("F1") },
+ { 1, 0, 1.0 },
+ { 0, 1, cxt.get_identifier_from_string("F2") },
+ { 1, 1, true },
+ { 0, 2, cxt.get_identifier_from_string("F3") },
+ { 1, 2, cxt.get_identifier_from_string("s1") },
+ { 0, 3, cxt.get_identifier_from_string("F4") },
+ { 1, 3 },
+ { 0, 4, cxt.get_identifier_from_string("F5") },
+ { 1, 4 },
+ };
+
+ assert(check_model_iterator_output(iter, checks));
+
+ // Iterate over the bottom 2 rows.
+
+ range.set_all_rows();
+ range.first.row = 8;
+ iter = cxt.get_model_iterator(0, rc_direction_t::vertical, range);
+
+ checks =
+ {
+ // row, column, value
+ { 8, 0, 1.7 },
+ { 9, 0, 1.8 },
+ { 8, 1, 199.9 },
+ { 9, 1, 299.9 },
+ { 8, 2 },
+ { 9, 2 },
+ { 8, 3, cxt.get_identifier_from_string("s8") },
+ { 9, 3, cxt.get_identifier_from_string("s9") },
+ { 8, 4 },
+ { 9, 4, cxt.get_identifier_from_string("end") },
+ };
+
+ assert(check_model_iterator_output(iter, checks));
+
+ // Iterate over the bottom-left corners.
+ range.last.column = 2;
+ iter = cxt.get_model_iterator(0, rc_direction_t::vertical, range);
+
+ checks =
+ {
+ // row, column, value
+ { 8, 0, 1.7 },
+ { 9, 0, 1.8 },
+ { 8, 1, 199.9 },
+ { 9, 1, 299.9 },
+ { 8, 2 },
+ { 9, 2 },
+ };
+
+ assert(check_model_iterator_output(iter, checks));
+
+ // Iterate over the top-right corners.
+ range.first.column = 3;
+ range.last.column = column_unset;
+ range.first.row = row_unset;
+ range.last.row = 1;
+ iter = cxt.get_model_iterator(0, rc_direction_t::vertical, range);
+
+ checks =
+ {
+ { 0, 3, cxt.get_identifier_from_string("F4") },
+ { 1, 3 },
+ { 0, 4, cxt.get_identifier_from_string("F5") },
+ { 1, 4 },
+ };
+
+ assert(check_model_iterator_output(iter, checks));
+
+ // Iterate over only one cell in the middle.
+ range.first.row = 5;
+ range.last.row = 5;
+ range.first.column = 3;
+ range.last.column = 3;
+
+ iter = cxt.get_model_iterator(0, rc_direction_t::vertical, range);
+ checks =
+ {
+ { 5, 3, cxt.get_identifier_from_string("s5") },
+ };
+
+ assert(check_model_iterator_output(iter, checks));
+}
+
+void test_model_context_iterator_named_exps()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ struct check
+ {
+ std::string name;
+ const named_expression_t* exp;
+ };
+
+ model_context cxt{{100, 10}};
+ cxt.append_sheet("test1");
+ cxt.append_sheet("test2");
+
+ named_expressions_iterator iter;
+ assert(!iter.has());
+ assert(iter.size() == 0);
+
+ iter = cxt.get_named_expressions_iterator();
+ assert(!iter.has());
+ assert(iter.size() == 0);
+
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::calc_a1, &cxt);
+ assert(resolver);
+
+ auto tokenize = [&](const char* p) -> formula_tokens_t
+ {
+ return parse_formula_string(cxt, abs_address_t(), *resolver, p);
+ };
+
+ auto validate = [](named_expressions_iterator _iter, const std::vector<check>& _expected) -> bool
+ {
+ if (_iter.size() != _expected.size())
+ {
+ cout << "iterator's size() returns wrong value." << endl;
+ return false;
+ }
+
+ for (const check& c : _expected)
+ {
+ if (!_iter.has())
+ {
+ cout << "iterator has no more element, but it is expected to." << endl;
+ return false;
+ }
+
+ if (c.name != *_iter.get().name)
+ {
+ cout << "names differ: expected='" << c.name << "'; actual='" << *_iter.get().name << endl;
+ return false;
+ }
+
+ if (c.exp != _iter.get().expression)
+ {
+ cout << "expressions differ." << endl;
+ return false;
+ }
+
+ _iter.next();
+ }
+
+ if (_iter.has())
+ {
+ cout << "the iterator has more elements, but it is not expected to." << endl;
+ return false;
+ }
+
+ return true;
+ };
+
+ cxt.set_named_expression("MyCalc", tokenize("(1+2)/3"));
+
+ std::vector<check> expected =
+ {
+ { "MyCalc", cxt.get_named_expression(0, "MyCalc") },
+ };
+
+ iter = cxt.get_named_expressions_iterator();
+ assert(validate(iter, expected));
+
+ cxt.set_named_expression("RefToRight", tokenize("B1"));
+
+ expected =
+ {
+ { "MyCalc", cxt.get_named_expression(0, "MyCalc") },
+ { "RefToRight", cxt.get_named_expression(0, "RefToRight") },
+ };
+
+ iter = cxt.get_named_expressions_iterator();
+ assert(validate(iter, expected));
+
+ cxt.set_named_expression(1, "MyCalc2", tokenize("(B1+C1)/D1"));
+ cxt.set_named_expression(1, "MyCalc3", tokenize("B1/(PI()*2)"));
+
+ iter = cxt.get_named_expressions_iterator(0);
+ assert(!iter.has());
+
+ iter = cxt.get_named_expressions_iterator(1);
+
+ expected =
+ {
+ { "MyCalc2", cxt.get_named_expression(1, "MyCalc2") },
+ { "MyCalc3", cxt.get_named_expression(1, "MyCalc3") },
+ };
+
+ assert(validate(iter, expected));
+}
+
+void test_model_context_fill_down()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ nullptr_t empty = nullptr;
+ model_context cxt{{100, 10}};
+ cxt.append_sheet("test");
+ cxt.set_cell_values(0, {
+ { "numeric", "bool", "string", "empty" },
+ { 12.3, true, "foo", empty },
+ { empty, empty, empty, 1.1 },
+ { empty, empty, empty, 1.1 },
+ { empty, empty, empty, 1.1 },
+ { empty, empty, empty, 1.1 },
+ { empty, empty, empty, 1.1 },
+ });
+
+ abs_address_t pos(0, 1, 0);
+ cxt.fill_down_cells(pos, 2);
+
+ assert(cxt.get_numeric_value(abs_address_t(0, 1, 0)) == 12.3);
+ assert(cxt.get_numeric_value(abs_address_t(0, 2, 0)) == 12.3);
+ assert(cxt.get_numeric_value(abs_address_t(0, 3, 0)) == 12.3);
+ assert(cxt.is_empty(abs_address_t(0, 4, 0)));
+
+ pos.column = 1;
+ cxt.fill_down_cells(pos, 1);
+ assert(cxt.get_boolean_value(abs_address_t(0, 1, 1)) == true);
+ assert(cxt.get_boolean_value(abs_address_t(0, 2, 1)) == true);
+ assert(cxt.is_empty(abs_address_t(0, 3, 1)));
+
+ pos.column = 2;
+ string_id_t s_foo = cxt.get_string_identifier(pos);
+ const std::string* p = cxt.get_string(s_foo);
+ assert(p && *p == "foo");
+ cxt.fill_down_cells(pos, 3);
+ assert(cxt.get_string_identifier(abs_address_t(0, 2, 2)) == s_foo);
+ assert(cxt.get_string_identifier(abs_address_t(0, 3, 2)) == s_foo);
+ assert(cxt.get_string_identifier(abs_address_t(0, 4, 2)) == s_foo);
+ assert(cxt.is_empty(abs_address_t(0, 5, 2)));
+
+ pos.column = 3;
+ cxt.fill_down_cells(pos, 2);
+ assert(cxt.is_empty(pos));
+ assert(cxt.is_empty(abs_address_t(0, 2, 3)));
+ assert(cxt.is_empty(abs_address_t(0, 3, 3)));
+ assert(cxt.get_numeric_value(abs_address_t(0, 4, 3)) == 1.1);
+}
+
+void test_model_context_error_value()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ model_context cxt{{100, 10}};
+ cxt.append_sheet("test");
+
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::excel_a1, &cxt);
+ assert(resolver);
+
+ abs_address_t pos(0,3,0);
+ const char* exp = "10/0";
+ formula_tokens_t tokens = parse_formula_string(cxt, pos, *resolver, exp);
+ formula_cell* fc = cxt.set_formula_cell(pos, std::move(tokens));
+ fc->interpret(cxt, pos);
+
+ cell_access ca = cxt.get_cell_access(pos);
+ assert(ca.get_type() == celltype_t::formula);
+ assert(ca.get_value_type() == cell_value_t::error);
+ assert(ca.get_error_value() == formula_error_t::division_by_zero);
+}
+
+void test_model_context_rename_sheets()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ model_context cxt{{100, 10}};
+ cxt.append_sheet("sheet1");
+ cxt.append_sheet("sheet2");
+ cxt.append_sheet("sheet3");
+
+ assert(cxt.get_sheet_index("sheet1") == 0);
+ assert(cxt.get_sheet_index("sheet2") == 1);
+ assert(cxt.get_sheet_index("sheet3") == 2);
+
+ cxt.set_sheet_name(0, "sheet1"); // Setting it to the current name is a no-op.
+ try
+ {
+ cxt.set_sheet_name(0, "sheet3");
+ assert(!"exception should have been thrown!");
+ }
+ catch (const ixion::model_context_error& e)
+ {
+ assert(e.get_error_type() == ixion::model_context_error::sheet_name_conflict);
+ }
+ catch (...)
+ {
+ assert(!"wrong exception caught");
+ }
+
+ cxt.set_sheet_name(0, "one");
+ cxt.set_sheet_name(1, "two");
+ cxt.set_sheet_name(2, "three");
+
+ assert(cxt.get_sheet_index("one") == 0);
+ assert(cxt.get_sheet_index("two") == 1);
+ assert(cxt.get_sheet_index("three") == 2);
+}
+
+void test_volatile_function()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ model_context cxt{{1048576, 16384}};
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::excel_a1, &cxt);
+ assert(resolver);
+
+ cxt.append_sheet("test");
+
+ abs_range_set_t dirty_cells;
+ abs_range_set_t modified_cells;
+
+ // Set values into A1:A3.
+ cxt.set_numeric_cell(abs_address_t(0,0,0), 1.0);
+ cxt.set_numeric_cell(abs_address_t(0,1,0), 2.0);
+ cxt.set_numeric_cell(abs_address_t(0,2,0), 3.0);
+
+ // Set formula in A4 that references A1:A3.
+ formula_cell* p = insert_formula(cxt, abs_address_t(0,3,0), "SUM(A1:A3)", *resolver);
+ assert(p);
+ dirty_cells.insert(abs_address_t(0,3,0));
+
+ // Initial full calculation.
+ auto sorted = ixion::query_and_sort_dirty_cells(cxt, modified_cells, &dirty_cells);
+ ixion::calculate_sorted_cells(cxt, sorted, 0);
+
+ double val = cxt.get_numeric_value(abs_address_t(0,3,0));
+ assert(val == 6);
+
+ modified_cells.clear();
+ dirty_cells.clear();
+
+ // Modify the value of A2. This should flag A4 dirty.
+ cxt.set_numeric_cell(abs_address_t(0,1,0), 10.0);
+ modified_cells.insert(abs_address_t(0,1,0));
+ sorted = ixion::query_and_sort_dirty_cells(cxt, modified_cells, &dirty_cells);
+ assert(sorted.size() == 1);
+
+ // Partial recalculation.
+ ixion::calculate_sorted_cells(cxt, sorted, 0);
+
+ val = cxt.get_numeric_value(abs_address_t(0, 3, 0));
+ assert(val == 14);
+
+ modified_cells.clear();
+ dirty_cells.clear();
+
+ // Insert a volatile cell into B1. At this point B1 should be the only dirty cell.
+ p = insert_formula(cxt, abs_address_t(0,0,1), "NOW()", *resolver);
+ assert(p);
+ dirty_cells.insert(abs_address_t(0,0,1));
+ sorted = ixion::query_and_sort_dirty_cells(cxt, modified_cells, &dirty_cells);
+ assert(sorted.size() == 1);
+
+ // Partial recalc again.
+ ixion::calculate_sorted_cells(cxt, sorted, 0);
+ double t1 = cxt.get_numeric_value(abs_address_t(0,0,1));
+
+ // Pause for 0.2 second.
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+
+ // No modification, but B1 should still be flagged dirty.
+ modified_cells.clear();
+ dirty_cells.clear();
+
+ sorted = ixion::query_and_sort_dirty_cells(cxt, modified_cells, &dirty_cells);
+ assert(sorted.size() == 1);
+ ixion::calculate_sorted_cells(cxt, sorted, 0);
+ double t2 = cxt.get_numeric_value(abs_address_t(0,0,1));
+ double delta = (t2-t1)*24*60*60;
+ cout << "delta = " << delta << endl;
+
+ // The delta should be close to 0.2. It may be a little larger depending
+ // on the CPU speed.
+ assert(0.2 <= delta && delta <= 0.3);
+}
+
+void test_invalid_formula_tokens()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ model_context cxt;
+ std::string_view invalid_formula("invalid formula");
+ std::string_view error_msg("failed to parse formula");
+
+ formula_tokens_t tokens = create_formula_error_tokens(cxt, invalid_formula, error_msg);
+
+ assert(tokens[0].opcode == fop_error);
+ assert(tokens.size() == (std::get<string_id_t>(tokens[0].value) + 1));
+
+ assert(tokens[1].opcode == fop_string);
+ string_id_t sid = std::get<string_id_t>(tokens[1].value);
+ const std::string* s = cxt.get_string(sid);
+ assert(invalid_formula == *s);
+
+ assert(tokens[2].opcode == fop_string);
+ sid = std::get<string_id_t>(tokens[2].value);
+ s = cxt.get_string(sid);
+ assert(error_msg == *s);
+}
+
+void test_grouped_formula_string_results()
+{
+ IXION_TEST_FUNC_SCOPE;
+
+ model_context cxt;
+ cxt.append_sheet("test");
+
+ auto resolver = formula_name_resolver::get(formula_name_resolver_t::excel_a1, &cxt);
+ assert(resolver);
+
+ abs_range_t A1B2(0, 0, 0, 2, 2);
+
+ formula_tokens_t tokens = parse_formula_string(cxt, A1B2.first, *resolver, "\"literal string\"");
+
+ matrix res_value(2, 2, std::string("literal string"));
+ formula_result res(std::move(res_value));
+ cxt.set_grouped_formula_cells(A1B2, std::move(tokens), std::move(res));
+
+ std::string_view s = cxt.get_string_value(A1B2.last);
+ assert(s == "literal string");
+}
+
+} // anonymous namespace
+
+int main()
+{
+ test_size();
+ test_string_to_double();
+ test_string_pool();
+ test_formula_tokens_store();
+ test_matrix();
+ test_matrix_non_numeric_values();
+
+ test_address();
+ test_parse_and_print_expressions();
+ test_function_name_resolution();
+ test_model_context_storage();
+ test_model_context_direct_string_access();
+ test_model_context_named_expression();
+ test_model_context_iterator_horizontal();
+ test_model_context_iterator_horizontal_range();
+ test_model_context_iterator_vertical();
+ test_model_context_iterator_vertical_range();
+ test_model_context_iterator_named_exps();
+ test_model_context_fill_down();
+ test_model_context_error_value();
+ test_model_context_rename_sheets();
+ test_volatile_function();
+ test_invalid_formula_tokens();
+ test_grouped_formula_string_results();
+
+ return EXIT_SUCCESS;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */