diff options
Diffstat (limited to 'src/orcus_test_xls_xml.cpp')
-rw-r--r-- | src/orcus_test_xls_xml.cpp | 2455 |
1 files changed, 2455 insertions, 0 deletions
diff --git a/src/orcus_test_xls_xml.cpp b/src/orcus_test_xls_xml.cpp new file mode 100644 index 0000000..71bbb17 --- /dev/null +++ b/src/orcus_test_xls_xml.cpp @@ -0,0 +1,2455 @@ +/* -*- 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 "orcus_test_global.hpp" +#include "filesystem_env.hpp" + +#include "orcus/orcus_xls_xml.hpp" +#include <orcus/format_detection.hpp> +#include "orcus/stream.hpp" +#include "orcus/config.hpp" +#include <orcus/parser_global.hpp> +#include "orcus/yaml_document_tree.hpp" +#include "orcus/spreadsheet/factory.hpp" +#include "orcus/spreadsheet/document.hpp" +#include "orcus/spreadsheet/view.hpp" +#include "orcus/spreadsheet/sheet.hpp" +#include "orcus/spreadsheet/shared_strings.hpp" +#include "orcus/spreadsheet/styles.hpp" +#include "orcus/spreadsheet/config.hpp" + +#include <ixion/model_context.hpp> +#include <ixion/address.hpp> +#include <ixion/cell.hpp> + +#include <string> +#include <sstream> +#include <vector> +#include <cmath> +#include <iostream> +#include <fstream> + +using namespace orcus; +namespace ss = orcus::spreadsheet; + +namespace { + +config test_config(format_t::xls_xml); + +const std::vector<fs::path> dirs = { + SRCDIR"/test/xls-xml/basic/", + SRCDIR"/test/xls-xml/basic-utf-16-be/", + SRCDIR"/test/xls-xml/basic-utf-16-le/", + SRCDIR"/test/xls-xml/bold-and-italic/", + SRCDIR"/test/xls-xml/colored-text/", + SRCDIR"/test/xls-xml/empty-rows/", + SRCDIR"/test/xls-xml/formula-array-1/", + SRCDIR"/test/xls-xml/formula-cells-1/", + SRCDIR"/test/xls-xml/formula-cells-2/", + SRCDIR"/test/xls-xml/formula-cells-3/", + SRCDIR"/test/xls-xml/invalid-sub-structure/", + SRCDIR"/test/xls-xml/leading-whitespace/", + SRCDIR"/test/xls-xml/merged-cells/", + SRCDIR"/test/xls-xml/named-colors/", + SRCDIR"/test/xls-xml/named-expression/", + SRCDIR"/test/xls-xml/named-expression-sheet-local/", + SRCDIR"/test/xls-xml/raw-values-1/", + SRCDIR"/test/xls-xml/table-offset/", + SRCDIR"/test/xls-xml/unnamed-parent-styles/", +}; + +std::unique_ptr<spreadsheet::document> load_doc_from_filepath( + const std::string& path, + bool recalc=true, + ss::formula_error_policy_t error_policy=ss::formula_error_policy_t::fail) +{ + std::cout << path << std::endl; + + spreadsheet::range_size_t ss{1048576, 16384}; + std::unique_ptr<spreadsheet::document> doc = std::make_unique<spreadsheet::document>(ss); + spreadsheet::import_factory factory(*doc); + factory.set_recalc_formula_cells(recalc); + factory.set_formula_error_policy(error_policy); + orcus_xls_xml app(&factory); + app.set_config(test_config); + app.read_file(path.c_str()); + + return doc; +} + +std::unique_ptr<spreadsheet::document> load_doc_from_stream(const std::string& path) +{ + spreadsheet::range_size_t ss{1048576, 16384}; + std::unique_ptr<spreadsheet::document> doc = std::make_unique<spreadsheet::document>(ss); + spreadsheet::import_factory factory(*doc); + orcus_xls_xml app(&factory); + + std::ifstream ifs(path, std::ios::binary | std::ios::ate); + std::streamsize n = ifs.tellg(); + ifs.seekg(0); + std::string content(n, '\0'); + if (ifs.read(content.data(), n)) + { + app.read_stream(content); + doc->recalc_formula_cells(); + } + + return doc; +} + +class doc_loader +{ + spreadsheet::document m_doc; + spreadsheet::import_factory m_factory; + +public: + doc_loader(std::string_view path) : + m_doc({1048576, 16384}), m_factory(m_doc) + { + std::cout << path << std::endl; + orcus_xls_xml app(&m_factory); + app.read_file(path.data()); + } + + spreadsheet::document& get_doc() + { + return m_doc; + } + + spreadsheet::import_factory& get_factory() + { + return m_factory; + } +}; + +void update_config(spreadsheet::document& doc, const std::string& path) +{ + try + { + spreadsheet::document_config cfg = doc.get_config(); + + yaml::document_tree config; + file_content content(path.data()); + config.load(content.str()); + yaml::const_node root = config.get_document_root(0); + std::vector<yaml::const_node> keys = root.keys(); + for (size_t i = 0; i < keys.size(); ++i) + { + const yaml::const_node& key = keys[i]; + if (key.type() == yaml::node_t::string && key.string_value() == "output-precision") + { + yaml::const_node child = root.child(i); + if (child.type() == yaml::node_t::number) + cfg.output_precision = child.numeric_value(); + } + } + + doc.set_config(cfg); + } + catch (const std::exception&) + { + // Do nothing. + } +} + +void test_xls_xml_detection() +{ + ORCUS_TEST_FUNC_SCOPE; + + for (const auto& dir : dirs) + { + fs::path filepath = dir / "input.xml"; + file_content fc(filepath.string()); + assert(!fc.empty()); + + format_t detected = detect(fc.str()); + assert(detected == format_t::xls_xml); + } +} + +void test_xls_xml_create_filter() +{ + ORCUS_TEST_FUNC_SCOPE; + + ss::range_size_t ssize{1048576, 16384}; + std::unique_ptr<ss::document> doc = std::make_unique<ss::document>(ssize); + ss::import_factory factory(*doc); + + auto f = create_filter(format_t::xls_xml, &factory); + assert(f); + assert(f->get_name() == "xls-xml"); +} + +void test_xls_xml_import() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto verify = [](spreadsheet::document& doc, const fs::path& dir) + { + auto path = dir / "config.yaml"; + update_config(doc, path.string()); + + // Dump the content of the model. + std::ostringstream os; + doc.dump_check(os); + std::string check = os.str(); + + // Check that against known control. + path = dir / "check.txt"; + file_content control(path.string()); + + assert(!check.empty()); + assert(!control.empty()); + + std::string_view s1(check.data(), check.size()); + std::string_view s2 = control.str(); + s1 = orcus::trim(s1); + s2 = orcus::trim(s2); + + if (s1 != s2) + { + size_t offset = locate_first_different_char(s1, s2); + auto line1 = locate_line_with_offset(s1, offset); + auto line2 = locate_line_with_offset(s2, offset); + std::cout << "expected: " << line2.line << std::endl; + std::cout << "observed: " << line1.line << std::endl; + assert(!"content verification failed"); + } + }; + + for (const auto& dir : dirs) + { + std::cout << dir << std::endl; + + // Read the input.xml document. + fs::path filepath = dir / "input.xml"; + std::unique_ptr<spreadsheet::document> doc = load_doc_from_filepath(filepath.string()); + verify(*doc, dir); + + doc = load_doc_from_stream(filepath.string()); + verify(*doc, dir); + } +} + +void test_xls_xml_merged_cells() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/merged-cells/input.xml"); + + const spreadsheet::sheet* sheet1 = doc->get_sheet("Sheet1"); + assert(sheet1); + + spreadsheet::range_t merge_range = sheet1->get_merge_cell_range(0, 1); + assert(merge_range.first.column == 1); + assert(merge_range.last.column == 2); + assert(merge_range.first.row == 0); + assert(merge_range.last.row == 0); + + merge_range = sheet1->get_merge_cell_range(0, 3); + assert(merge_range.first.column == 3); + assert(merge_range.last.column == 5); + assert(merge_range.first.row == 0); + assert(merge_range.last.row == 0); + + merge_range = sheet1->get_merge_cell_range(1, 0); + assert(merge_range.first.column == 0); + assert(merge_range.last.column == 0); + assert(merge_range.first.row == 1); + assert(merge_range.last.row == 2); + + merge_range = sheet1->get_merge_cell_range(3, 0); + assert(merge_range.first.column == 0); + assert(merge_range.last.column == 0); + assert(merge_range.first.row == 3); + assert(merge_range.last.row == 5); + + merge_range = sheet1->get_merge_cell_range(2, 2); + assert(merge_range.first.column == 2); + assert(merge_range.last.column == 5); + assert(merge_range.first.row == 2); + assert(merge_range.last.row == 5); +} + +void test_xls_xml_date_time() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/date-time/input.xml"); + + const spreadsheet::sheet* sheet1 = doc->get_sheet("Sheet1"); + assert(sheet1); + + // B1 contains date-only value. + date_time_t dt = sheet1->get_date_time(0, 1); + assert(dt == date_time_t(2016, 12, 14)); + + // B2 contains date-time value with no fraction seconds. + dt = sheet1->get_date_time(1, 1); + assert(dt == date_time_t(2002, 2, 3, 12, 34, 45)); + + // B3 contains date-time value with fraction second (1992-03-04 08:34:33.555) + dt = sheet1->get_date_time(2, 1); + assert(dt.year == 1992); + assert(dt.month == 3); + assert(dt.day == 4); + assert(dt.hour == 8); + assert(dt.minute == 34); + assert(std::floor(dt.second) == 33.0); + + // Evalutate the fraction second as milliseconds. + double ms = dt.second * 1000.0; + ms -= std::floor(dt.second) * 1000.0; + ms = std::round(ms); + assert(ms == 555.0); +} + +void test_xls_xml_bold_and_italic() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/bold-and-italic/input.xml"); + + const spreadsheet::sheet* sheet1 = doc->get_sheet("Sheet1"); + assert(sheet1); + + const spreadsheet::shared_strings& ss = doc->get_shared_strings(); + + const spreadsheet::styles& styles = doc->get_styles(); + + // A1 contains unformatted text. + size_t si = sheet1->get_string_identifier(0, 0); + const std::string* sp = ss.get_string(si); + assert(sp); + assert(*sp == "Normal Text"); + + // A2 contains bold text. + si = sheet1->get_string_identifier(1, 0); + sp = ss.get_string(si); + assert(sp); + assert(*sp == "Bold Text"); + + size_t xfi = sheet1->get_cell_format(1, 0); + const spreadsheet::cell_format_t* cf = styles.get_cell_format(xfi); + assert(cf); + const spreadsheet::font_t* font = styles.get_font(cf->font); + assert(font); + assert(*font->bold); + assert(!*font->italic); + + // A3 contains italic text. + si = sheet1->get_string_identifier(2, 0); + sp = ss.get_string(si); + assert(sp); + assert(*sp == "Italic Text"); + + xfi = sheet1->get_cell_format(2, 0); + cf = styles.get_cell_format(xfi); + assert(cf); + font = styles.get_font(cf->font); + assert(font); + assert(!*font->bold); + assert(*font->italic); + + // A4 contains bold and italic text. + si = sheet1->get_string_identifier(3, 0); + sp = ss.get_string(si); + assert(sp); + assert(*sp == "Bold and Italic Text"); + + xfi = sheet1->get_cell_format(3, 0); + cf = styles.get_cell_format(xfi); + assert(cf); + font = styles.get_font(cf->font); + assert(font); + assert(font->bold); + assert(font->italic); + + // A5 contains a mixed format text. + si = sheet1->get_string_identifier(4, 0); + sp = ss.get_string(si); + assert(sp); + assert(*sp == "Bold and Italic mixed"); + + // The string contains 4 formatted segments. + const spreadsheet::format_runs_t* fmt_runs = ss.get_format_runs(si); + assert(fmt_runs); + assert(fmt_runs->size() == 4); + + // First formatted segment is bold. + const spreadsheet::format_run* fmt_run = &fmt_runs->at(0); + assert(fmt_run->pos == 0); + assert(fmt_run->size == 4); + assert(fmt_run->bold); + assert(!fmt_run->italic); + + // Third formatted segment is italic. + fmt_run = &fmt_runs->at(2); + assert(fmt_run->pos == 9); + assert(fmt_run->size == 6); + assert(!fmt_run->bold); + assert(fmt_run->italic); +} + +void test_xls_xml_colored_text() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/colored-text/input.xml"); + + const spreadsheet::sheet* sheet1 = doc->get_sheet("ColoredText"); + assert(sheet1); + + const spreadsheet::shared_strings& ss = doc->get_shared_strings(); + + const spreadsheet::styles& styles = doc->get_styles(); + + // Column A contains colored cells. + + struct check + { + spreadsheet::row_t row; + spreadsheet::color_elem_t red; + spreadsheet::color_elem_t green; + spreadsheet::color_elem_t blue; + std::string text; + }; + + std::vector<check> checks = { + { 1, 0xC0, 0x00, 0x00, "Dark Red" }, + { 2, 0xFF, 0x00, 0x00, "Red" }, + { 3, 0xFF, 0xC0, 0x00, "Orange" }, + { 4, 0xFF, 0xFF, 0x00, "Yellow" }, + { 5, 0x92, 0xD0, 0x50, "Light Green" }, + { 6, 0x00, 0xB0, 0x50, "Green" }, + { 7, 0x00, 0xB0, 0xF0, "Light Blue" }, + { 8, 0x00, 0x70, 0xC0, "Blue" }, + { 9, 0x00, 0x20, 0x60, "Dark Blue" }, + { 10, 0x70, 0x30, 0xA0, "Purple" }, + }; + + for (const check& c : checks) + { + size_t xfi = sheet1->get_cell_format(c.row, 0); + const spreadsheet::cell_format_t* xf = styles.get_cell_format(xfi); + assert(xf); + + const spreadsheet::font_t* font = styles.get_font(xf->font); + assert(font); + assert(font->color); + assert(font->color.value().red == c.red); + assert(font->color.value().green == c.green); + assert(font->color.value().blue == c.blue); + + size_t si = sheet1->get_string_identifier(c.row, 0); + const std::string* s = ss.get_string(si); + assert(s); + assert(*s == c.text); + } + + // Cell B2 contains mix-colored text. + size_t si = sheet1->get_string_identifier(1, 1); + const std::string* s = ss.get_string(si); + assert(s); + assert(*s == "Red and Blue"); + const spreadsheet::format_runs_t* fmt_runs = ss.get_format_runs(si); + assert(fmt_runs); + + // There should be 2 segments that are color-formatted and one that is not. + assert(fmt_runs->size() == 3); + + // The 'Red' segment should be in red color. + const spreadsheet::format_run* fmt = &fmt_runs->at(0); + assert(fmt->color.alpha == 0xFF); + assert(fmt->color.red == 0xFF); + assert(fmt->color.green == 0); + assert(fmt->color.blue == 0); + assert(fmt->pos == 0); + assert(fmt->size == 3); + + // The 'Blue' segment should be in blue color. + fmt = &fmt_runs->at(2); + assert(fmt->color.alpha == 0xFF); + assert(fmt->color.red == 0); + assert(fmt->color.green == 0x70); + assert(fmt->color.blue == 0xC0); + assert(fmt->pos == 8); + assert(fmt->size == 4); +} + +void test_xls_xml_formatted_text_basic() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/formatted-text/basic.xml"); + const auto& styles_pool = doc->get_styles(); + + auto get_font = [&styles_pool](const ss::sheet& sh, ss::row_t row, ss::col_t col) + { + std::size_t xf = sh.get_cell_format(row, col); + + const ss::cell_format_t* cell_format = styles_pool.get_cell_format(xf); + assert(cell_format); + + const ss::font_t* font = styles_pool.get_font(cell_format->font); + assert(font); + + return font; + }; + + auto check_cell_bold = [&get_font](const ss::sheet& sh, ss::row_t row, ss::col_t col, bool expected) + { + const ss::font_t* font = get_font(sh, row, col); + + if (expected) + { + if (font->bold && *font->bold) + return true; + + std::cerr << "expected to be bold but it is not " + << "(sheet=" << sh.get_index() << "; row=" << row << "; column=" << col << ")" + << std::endl; + + return false; + } + else + { + if (!font->bold || !*font->bold) + return true; + + std::cerr << "expected to be non-bold but it is bold " + << "(sheet=" << sh.get_index() << "; row=" << row << "; column=" << col << ")" + << std::endl; + + return false; + } + }; + + auto check_cell_italic = [&get_font](const ss::sheet& sh, ss::row_t row, ss::col_t col, bool expected) + { + const ss::font_t* font = get_font(sh, row, col); + + if (expected) + { + if (font->italic && *font->italic) + return true; + + std::cerr << "expected to be italic but it is not " + << "(sheet=" << sh.get_index() << "; row=" << row << "; column=" << col << ")" + << std::endl; + + return false; + } + else + { + if (!font->italic || !*font->italic) + return true; + + std::cerr << "expected to be non-italic but it is italic " + << "(sheet=" << sh.get_index() << "; row=" << row << "; column=" << col << ")" + << std::endl; + + return false; + } + }; + + auto check_cell_text = [&doc](const ss::sheet& sh, ss::row_t row, ss::col_t col, std::string_view expected) + { + const auto& sstrings = doc->get_shared_strings(); + + std::size_t si = sh.get_string_identifier(row, col); + const std::string* s = sstrings.get_string(si); + if (!s) + { + std::cerr << "expected='" << expected << "'; actual=<none> " + << "(sheet=" << sh.get_index() << "; row=" << row << "; column=" << col << ")" + << std::endl; + + return false; + } + + if (*s == expected) + return true; + + std::cerr << "expected='" << expected << "'; actual='" << *s << "' " + << "(sheet=" << sh.get_index() << "; row=" << row << "; column=" << col << ")" + << std::endl; + + return false; + }; + + { + const spreadsheet::sheet* sheet = doc->get_sheet("Text Properties"); + assert(sheet); + + ss::row_t row = 0; + ss::col_t col = 0; + + // A1 - unformatted + assert(check_cell_text(*sheet, row, col, "Normal Text")); + assert(check_cell_bold(*sheet, row, col, false)); + assert(check_cell_italic(*sheet, row, col, false)); + + // A2 - bold + row = 1; + assert(check_cell_text(*sheet, row, col, "Bold Text")); + assert(check_cell_bold(*sheet, row, col, true)); + assert(check_cell_italic(*sheet, row, col, false)); + + // A3 - italic + row = 2; + assert(check_cell_text(*sheet, row, col, "Italic Text")); + assert(check_cell_bold(*sheet, row, col, false)); + assert(check_cell_italic(*sheet, row, col, true)); + + // A4 - bold and italic + row = 3; + assert(check_cell_text(*sheet, row, col, "Bold and Italic Text")); + assert(check_cell_bold(*sheet, row, col, true)); + assert(check_cell_italic(*sheet, row, col, true)); + + // A5 - bold and italic mixed. Excel creates format runs even for + // non-formatted segments. + row = 4; + assert(check_cell_text(*sheet, row, col, "Bold and Italic mixed")); + + std::size_t si = sheet->get_string_identifier(row, col); + const ss::format_runs_t* runs = doc->get_shared_strings().get_format_runs(si); + assert(runs); + assert(runs->size() == 4u); // only 0 and 2 are formatted + + // Bold and ... + // ^^^^ + assert(runs->at(0).pos == 0); + assert(runs->at(0).size == 4); + assert(runs->at(0).bold); + assert(!runs->at(0).italic); + + // Bold and Italic + // ^^^^^^ + assert(runs->at(2).pos == 9); + assert(runs->at(2).size == 6); + assert(!runs->at(2).bold); + assert(runs->at(2).italic); + + // A6 + row = 5; + assert(check_cell_text(*sheet, row, col, "Bold base with non-bold part")); + assert(check_cell_bold(*sheet, row, col, true)); + assert(check_cell_italic(*sheet, row, col, false)); + + si = sheet->get_string_identifier(row, col); + runs = doc->get_shared_strings().get_format_runs(si); + assert(runs); + assert(runs->size() == 3u); + + assert(runs->at(0).pos == 0); + assert(runs->at(0).size == 15); + assert(runs->at(0).bold); + + assert(runs->at(1).pos == 15); + assert(runs->at(1).size == 8); + assert(!runs->at(1).bold); + + assert(runs->at(2).pos == 23); + assert(runs->at(2).size == 5); + assert(runs->at(2).bold); + + // A7 - TODO: check format + row = 6; + assert(check_cell_text(*sheet, row, col, "Only partially underlined")); + + // A8 + row = 7; + assert(check_cell_text(*sheet, row, col, "All Underlined")); + const ss::font_t* font = get_font(*sheet, row, col); + assert(font->underline_style); + assert(*font->underline_style == ss::underline_t::single_line); + + // A9 + row = 8; + assert(check_cell_text(*sheet, row, col, "Bold and underlined")); + assert(check_cell_bold(*sheet, row, col, true)); + font = get_font(*sheet, row, col); + assert(font->underline_style); + assert(*font->underline_style == ss::underline_t::single_line); + + row = 9; + assert(check_cell_text(*sheet, row, col, "All Strikethrough")); + // TODO: check for strikethrough in cell + + // A11:A15 - TODO: check format + row = 10; + assert(check_cell_text(*sheet, row, col, "Partial strikethrough")); + row = 11; + assert(check_cell_text(*sheet, row, col, "Superscript")); + row = 12; + assert(check_cell_text(*sheet, row, col, "Subscript")); + row = 13; + assert(check_cell_text(*sheet, row, col, "x2 + y2 = 102")); + row = 14; + assert(check_cell_text(*sheet, row, col, "xi = yi + zi")); + } + + { + const spreadsheet::sheet* sheet = doc->get_sheet("Fonts"); + assert(sheet); + + struct check + { + ss::row_t row; + std::string_view font_name; + double font_unit; + }; + + check checks[] = { + { 0, "Calibri Light", 12.0 }, + { 1, "Arial", 18.0 }, + { 2, "Times New Roman", 14.0 }, + { 3, "Consolas", 9.0 }, + { 4, "Bookman Old Style", 20.0 }, + }; + + for (const auto& c : checks) + { + std::size_t xf = sheet->get_cell_format(c.row, 0); + const ss::cell_format_t* cell_format = styles_pool.get_cell_format(xf); + assert(cell_format); + const ss::font_t* font = styles_pool.get_font(cell_format->font); + assert(font); + assert(font->name == c.font_name); + assert(font->size == c.font_unit); + + // Columns A and B should have the same font. + xf = sheet->get_cell_format(c.row, 1); + cell_format = styles_pool.get_cell_format(xf); + assert(cell_format); + font = styles_pool.get_font(cell_format->font); + assert(font); + assert(font->name == c.font_name); + assert(font->size == c.font_unit); + } + } + + { + const spreadsheet::sheet* sheet = doc->get_sheet("Mixed Fonts"); + assert(sheet); + + // A1 + ss::row_t row = 0; + ss::col_t col = 0; + assert(check_cell_text(*sheet, row, col, "C++ has class and struct as keywords.")); + + // Base cell has Serif 12-pt font applied + auto xf = sheet->get_cell_format(row, col); + const ss::cell_format_t* fmt = styles_pool.get_cell_format(xf); + assert(fmt); + const ss::font_t* font = styles_pool.get_font(fmt->font); + assert(font); + assert(font->name == "Calibri"); + assert(font->size == 11.0); + + // Two segments has Liberation Mono font applied (runs 1 and 3) whereas + // runs 0, 2 and 4 are unformatted. + std::size_t si = sheet->get_string_identifier(row, col); + const ss::format_runs_t* runs = doc->get_shared_strings().get_format_runs(si); + assert(runs); + assert(runs->size() == 5u); + + // C++ has class ... + // ^^^^^ + assert(runs->at(1).pos == 8); + assert(runs->at(1).size == 5); + assert(runs->at(1).font == "Liberation Mono"); + + // ... and struct as ... + // ^^^^^^ + assert(runs->at(3).pos == 18); + assert(runs->at(3).size == 6); + assert(runs->at(3).font == "Liberation Mono"); + + // A2 + row = 1; + assert(check_cell_text(*sheet, row, col, "Text with 12-point font, 24-point font, and 36-point font mixed.")); + si = sheet->get_string_identifier(row, col); + runs = doc->get_shared_strings().get_format_runs(si); + assert(runs); + assert(runs->size() == 10u); + + // with 12-point font, ... + // ^^ + assert(runs->at(1).pos == 10); + assert(runs->at(1).size == 2); + assert(runs->at(1).font_size == 12.0f); + assert(runs->at(1).color == ss::color_t(0xFF, 0xFF, 0, 0)); // red + + // with 12-point font, ... + // ^^^^^^ + assert(runs->at(2).pos == 12); + assert(runs->at(2).size == 6); + assert(runs->at(2).font_size == 12.0f); + + // 24-point font, + // ^^ + assert(runs->at(4).pos == 25); + assert(runs->at(4).size == 2); + assert(runs->at(4).font_size == 24.0f); + assert(runs->at(4).color == ss::color_t(0xFF, 0xFF, 0, 0)); // red + + // 24-point font, + // ^^^^^^ + assert(runs->at(5).pos == 27); + assert(runs->at(5).size == 6); + assert(runs->at(5).font_size == 24.0f); + + // and 36-point font + // ^^ + assert(runs->at(7).pos == 44); + assert(runs->at(7).size == 2); + assert(runs->at(7).font_size == 36.0f); + assert(runs->at(7).color == ss::color_t(0xFF, 0xFF, 0, 0)); // red + + // and 36-point font + // ^^^^^^ + assert(runs->at(8).pos == 46); + assert(runs->at(8).size == 6); + assert(runs->at(8).font_size == 36.0f); + } +} + +void test_xls_xml_column_width_row_height() +{ + ORCUS_TEST_FUNC_SCOPE; + + struct cw_check + { + spreadsheet::col_t col; + double width; + int decimals; + }; + + struct rh_check + { + spreadsheet::row_t row; + double height; + int decimals; + }; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/column-width-row-height/input.xml"); + + // Column widths and row heights are stored in twips. Convert them to + // points so that we can compare them with the values stored in the source + // file. + + { + // Sheet1 + const spreadsheet::sheet* sheet = doc->get_sheet(0); + assert(sheet); + + std::vector<cw_check> cw_checks = + { + { 1, 56.25, 2 }, + { 2, 82.50, 2 }, + { 3, 108.75, 2 }, + { 5, 66.75, 2 }, + { 6, 66.75, 2 }, + { 7, 66.75, 2 }, + { 10, 119.25, 2 }, + { 11, 119.25, 2 }, + }; + + for (const cw_check& check : cw_checks) + { + spreadsheet::col_width_t cw = sheet->get_col_width(check.col, nullptr, nullptr); + double pt = convert(cw, length_unit_t::twip, length_unit_t::point); + test::verify_value_to_decimals(__FILE__, __LINE__, check.width, pt, check.decimals); + } + + std::vector<rh_check> rh_checks = + { + { 2, 20.0, 0 }, + { 3, 30.0, 0 }, + { 4, 40.0, 0 }, + { 5, 50.0, 0 }, + { 7, 25.0, 0 }, + { 8, 25.0, 0 }, + { 9, 25.0, 0 }, + { 12, 35.0, 0 }, + { 13, 35.0, 0 }, + }; + + for (const rh_check& check : rh_checks) + { + spreadsheet::row_height_t rh = sheet->get_row_height(check.row, nullptr, nullptr); + double pt = convert(rh, length_unit_t::twip, length_unit_t::point); + test::verify_value_to_decimals(__FILE__, __LINE__, check.height, pt, check.decimals); + } + } + + { + // Sheet2 + const spreadsheet::sheet* sheet = doc->get_sheet(1); + assert(sheet); + + std::vector<cw_check> cw_checks = + { + { 1, 119.25, 2 }, + { 3, 234.75, 2 }, + }; + + for (const cw_check& check : cw_checks) + { + spreadsheet::col_width_t cw = sheet->get_col_width(check.col, nullptr, nullptr); + double pt = convert(cw, length_unit_t::twip, length_unit_t::point); + test::verify_value_to_decimals(__FILE__, __LINE__, check.width, pt, check.decimals); + } + + std::vector<rh_check> rh_checks = + { + { 2, 40.0, 0 }, + { 4, 60.0, 0 }, + }; + + for (const rh_check& check : rh_checks) + { + spreadsheet::row_height_t rh = sheet->get_row_height(check.row, nullptr, nullptr); + double pt = convert(rh, length_unit_t::twip, length_unit_t::point); + test::verify_value_to_decimals(__FILE__, __LINE__, check.height, pt, check.decimals); + } + } +} + +void test_xls_xml_background_fill() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/background-color/standard.xml"); + + spreadsheet::styles& styles = doc->get_styles(); + + spreadsheet::sheet* sh = doc->get_sheet(0); + assert(sh); + + struct check + { + spreadsheet::row_t row; + spreadsheet::col_t col; + spreadsheet::fill_pattern_t pattern_type; + spreadsheet::color_t fg_color; + }; + + std::vector<check> checks = + { + { 1, 0, spreadsheet::fill_pattern_t::solid, { 255, 192, 0, 0 } }, // A2 - dark red + { 2, 0, spreadsheet::fill_pattern_t::solid, { 255, 255, 0, 0 } }, // A3 - red + { 3, 0, spreadsheet::fill_pattern_t::solid, { 255, 255, 192, 0 } }, // A4 - orange + { 4, 0, spreadsheet::fill_pattern_t::solid, { 255, 255, 255, 0 } }, // A5 - yellow + { 5, 0, spreadsheet::fill_pattern_t::solid, { 255, 146, 208, 80 } }, // A6 - light green + { 6, 0, spreadsheet::fill_pattern_t::solid, { 255, 0, 176, 80 } }, // A7 - green + { 7, 0, spreadsheet::fill_pattern_t::solid, { 255, 0, 176, 240 } }, // A8 - light blue + { 8, 0, spreadsheet::fill_pattern_t::solid, { 255, 0, 112, 192 } }, // A9 - blue + { 9, 0, spreadsheet::fill_pattern_t::solid, { 255, 0, 32, 96 } }, // A10 - dark blue + { 10, 0, spreadsheet::fill_pattern_t::solid, { 255, 112, 48, 160 } }, // A11 - purple + }; + + spreadsheet::color_t color_white(255, 255, 255, 255); + + for (const check& c : checks) + { + size_t xf = sh->get_cell_format(c.row, c.col); + + const spreadsheet::cell_format_t* cf = styles.get_cell_format(xf); + assert(cf); + + const spreadsheet::fill_t* fill_data = styles.get_fill(cf->fill); + assert(fill_data); + assert(fill_data->pattern_type == c.pattern_type); + assert(fill_data->fg_color == c.fg_color); + + // The font colors are all white in the colored cells. + const spreadsheet::font_t* font_data = styles.get_font(cf->font); + assert(font_data); + + assert(font_data->color == color_white); + } +} + +void test_xls_xml_named_colors() +{ + ORCUS_TEST_FUNC_SCOPE; + + constexpr std::string_view paths[] = { + SRCDIR"/test/xls-xml/named-colors/input.xml", + SRCDIR"/test/xls-xml/named-colors/input-upper.xml" + }; + + for (auto path : paths) + { + std::cout << path << std::endl; + std::unique_ptr<spreadsheet::document> doc = load_doc_from_filepath(std::string{path}); + + spreadsheet::styles& styles = doc->get_styles(); + const ixion::model_context& model = doc->get_model_context(); + + spreadsheet::sheet* sh = doc->get_sheet(0); + assert(sh); + + for (ss::row_t row = 1; row < 141; ++row) + { + // Column B stores the expected RGB value in hex. + size_t sid = model.get_string_identifier(ixion::abs_address_t(sh->get_index(), row, 1)); + const std::string* s = model.get_string(sid); + assert(s); + spreadsheet::color_rgb_t expected = spreadsheet::to_color_rgb(*s); + + size_t xf = sh->get_cell_format(row, 0); + const ss::fill_t* fill_data = styles.get_fill(xf); + assert(fill_data->fg_color); + const ss::color_t& actual = *fill_data->fg_color; + assert(expected.red == actual.red); + assert(expected.green == actual.green); + assert(expected.blue == actual.blue); + } + } +} + +void test_xls_xml_text_alignment() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/text-alignment/input.xml"); + + spreadsheet::styles& styles = doc->get_styles(); + + spreadsheet::sheet* sh = doc->get_sheet(0); + assert(sh); + + struct check + { + spreadsheet::row_t row; + spreadsheet::col_t col; + bool apply_align; + spreadsheet::hor_alignment_t hor_align; + spreadsheet::ver_alignment_t ver_align; + }; + + std::vector<check> checks = + { + { 1, 2, true, spreadsheet::hor_alignment_t::unknown, spreadsheet::ver_alignment_t::bottom }, // C2 + { 2, 2, true, spreadsheet::hor_alignment_t::left, spreadsheet::ver_alignment_t::bottom }, // C3 + { 3, 2, true, spreadsheet::hor_alignment_t::center, spreadsheet::ver_alignment_t::bottom }, // C4 + { 4, 2, true, spreadsheet::hor_alignment_t::right, spreadsheet::ver_alignment_t::bottom }, // C5 + { 5, 2, true, spreadsheet::hor_alignment_t::left, spreadsheet::ver_alignment_t::bottom }, // C6 + { 6, 2, true, spreadsheet::hor_alignment_t::left, spreadsheet::ver_alignment_t::bottom }, // C7 + { 7, 2, true, spreadsheet::hor_alignment_t::right, spreadsheet::ver_alignment_t::bottom }, // C8 + { 8, 2, true, spreadsheet::hor_alignment_t::right, spreadsheet::ver_alignment_t::bottom }, // C9 + { 9, 2, true, spreadsheet::hor_alignment_t::unknown, spreadsheet::ver_alignment_t::middle }, // C10 + { 10, 2, true, spreadsheet::hor_alignment_t::left, spreadsheet::ver_alignment_t::middle }, // C11 + { 11, 2, true, spreadsheet::hor_alignment_t::center, spreadsheet::ver_alignment_t::middle }, // C12 + { 12, 2, true, spreadsheet::hor_alignment_t::right, spreadsheet::ver_alignment_t::middle }, // C13 + { 13, 2, true, spreadsheet::hor_alignment_t::left, spreadsheet::ver_alignment_t::middle }, // C14 + { 14, 2, true, spreadsheet::hor_alignment_t::left, spreadsheet::ver_alignment_t::middle }, // C15 + { 15, 2, true, spreadsheet::hor_alignment_t::right, spreadsheet::ver_alignment_t::middle }, // C16 + { 16, 2, true, spreadsheet::hor_alignment_t::right, spreadsheet::ver_alignment_t::middle }, // C17 + { 17, 2, true, spreadsheet::hor_alignment_t::unknown, spreadsheet::ver_alignment_t::top }, // C18 + { 18, 2, true, spreadsheet::hor_alignment_t::left, spreadsheet::ver_alignment_t::top }, // C19 + { 19, 2, true, spreadsheet::hor_alignment_t::center, spreadsheet::ver_alignment_t::top }, // C20 + { 20, 2, true, spreadsheet::hor_alignment_t::right, spreadsheet::ver_alignment_t::top }, // C21 + { 21, 2, true, spreadsheet::hor_alignment_t::left, spreadsheet::ver_alignment_t::top }, // C22 + { 22, 2, true, spreadsheet::hor_alignment_t::left, spreadsheet::ver_alignment_t::top }, // C23 + { 23, 2, true, spreadsheet::hor_alignment_t::right, spreadsheet::ver_alignment_t::top }, // C24 + { 24, 2, true, spreadsheet::hor_alignment_t::right, spreadsheet::ver_alignment_t::top }, // C25 + { 25, 2, true, spreadsheet::hor_alignment_t::unknown, spreadsheet::ver_alignment_t::justified }, // C26 + { 26, 2, true, spreadsheet::hor_alignment_t::justified, spreadsheet::ver_alignment_t::bottom }, // C27 + { 27, 2, true, spreadsheet::hor_alignment_t::distributed, spreadsheet::ver_alignment_t::distributed }, // C28 + }; + + for (const check& c : checks) + { + std::cout << "row=" << c.row << "; col=" << c.col << std::endl; + size_t xf = sh->get_cell_format(c.row, c.col); + + const spreadsheet::cell_format_t* cf = styles.get_cell_format(xf); + assert(cf); + assert(c.apply_align == cf->apply_alignment); + + if (!cf->apply_alignment) + continue; + + assert(c.hor_align == cf->hor_align); + assert(c.ver_align == cf->ver_align); + } +} + +void test_xls_xml_cell_borders_single_cells() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/borders/single-cells.xml"); + + spreadsheet::styles& styles = doc->get_styles(); + + spreadsheet::sheet* sh = doc->get_sheet(0); + assert(sh); + + struct check + { + spreadsheet::row_t row; + spreadsheet::col_t col; + spreadsheet::border_style_t style; + }; + + std::vector<check> checks = + { + { 3, 1, spreadsheet::border_style_t::hair }, + { 5, 1, spreadsheet::border_style_t::dotted }, + { 7, 1, spreadsheet::border_style_t::dash_dot_dot }, + { 9, 1, spreadsheet::border_style_t::dash_dot }, + { 11, 1, spreadsheet::border_style_t::dashed }, + { 13, 1, spreadsheet::border_style_t::thin }, + { 1, 3, spreadsheet::border_style_t::medium_dash_dot_dot }, + { 3, 3, spreadsheet::border_style_t::slant_dash_dot }, + { 5, 3, spreadsheet::border_style_t::medium_dash_dot }, + { 7, 3, spreadsheet::border_style_t::medium_dashed }, + { 9, 3, spreadsheet::border_style_t::medium }, + { 11, 3, spreadsheet::border_style_t::thick }, + { 13, 3, spreadsheet::border_style_t::double_border }, + }; + + for (const check& c : checks) + { + std::cout << "(row: " << c.row << "; col: " << c.col << "; expected: " << int(c.style) << ")" << std::endl; + size_t xf = sh->get_cell_format(c.row, c.col); + const spreadsheet::cell_format_t* cf = styles.get_cell_format(xf); + assert(cf); + assert(cf->apply_border); + + const spreadsheet::border_t* border = styles.get_border(cf->border); + assert(border); + assert(border->top.style == c.style); + assert(border->bottom.style == c.style); + assert(border->left.style == c.style); + assert(border->right.style == c.style); + } +} + +void test_xls_xml_cell_borders_directions() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/borders/directions.xml"); + + spreadsheet::styles& styles = doc->get_styles(); + + spreadsheet::sheet* sh = doc->get_sheet(0); + assert(sh); + + struct check + { + spreadsheet::row_t row; + spreadsheet::col_t col; + spreadsheet::border_direction_t dir; + }; + + std::vector<check> checks = + { + { 1, 1, ss::border_direction_t::top }, + { 3, 1, ss::border_direction_t::left }, + { 5, 1, ss::border_direction_t::right }, + { 7, 1, ss::border_direction_t::bottom }, + { 9, 1, ss::border_direction_t::diagonal_tl_br }, + { 11, 1, ss::border_direction_t::diagonal_bl_tr }, + { 13, 1, ss::border_direction_t::diagonal }, + }; + + for (const check& c : checks) + { + size_t xf = sh->get_cell_format(c.row, c.col); + const ss::cell_format_t* cf = styles.get_cell_format(xf); + assert(cf); + assert(cf->apply_border); + + const ss::border_t* border = styles.get_border(cf->border); + assert(border); + + switch (c.dir) + { + case ss::border_direction_t::top: + assert(border->top.style); + assert(*border->top.style == ss::border_style_t::thin); + assert(!border->top.border_color); + assert(!border->top.border_width); + assert(!border->bottom.style); + assert(!border->bottom.border_color); + assert(!border->bottom.border_width); + assert(!border->left.style); + assert(!border->left.border_color); + assert(!border->left.border_width); + assert(!border->right.style); + assert(!border->right.border_color); + assert(!border->right.border_width); + assert(!border->diagonal.style); + assert(!border->diagonal.border_color); + assert(!border->diagonal.border_width); + assert(!border->diagonal_bl_tr.style); + assert(!border->diagonal_bl_tr.border_color); + assert(!border->diagonal_bl_tr.border_width); + assert(!border->diagonal_tl_br.style); + assert(!border->diagonal_tl_br.border_color); + assert(!border->diagonal_tl_br.border_width); + break; + case ss::border_direction_t::left: + assert(!border->top.style); + assert(!border->top.border_color); + assert(!border->top.border_width); + assert(!border->bottom.style); + assert(!border->bottom.border_color); + assert(!border->bottom.border_width); + assert(border->left.style); + assert(*border->left.style == ss::border_style_t::thin); + assert(!border->left.border_color); + assert(!border->left.border_width); + assert(!border->right.style); + assert(!border->right.border_color); + assert(!border->right.border_width); + assert(!border->diagonal.style); + assert(!border->diagonal.border_color); + assert(!border->diagonal.border_width); + assert(!border->diagonal_bl_tr.style); + assert(!border->diagonal_bl_tr.border_color); + assert(!border->diagonal_bl_tr.border_width); + assert(!border->diagonal_tl_br.style); + assert(!border->diagonal_tl_br.border_color); + assert(!border->diagonal_tl_br.border_width); + break; + case ss::border_direction_t::right: + assert(!border->top.style); + assert(!border->top.border_color); + assert(!border->top.border_width); + assert(!border->bottom.style); + assert(!border->bottom.border_color); + assert(!border->bottom.border_width); + assert(!border->left.style); + assert(!border->left.border_color); + assert(!border->left.border_width); + assert(border->right.style); + assert(*border->right.style == ss::border_style_t::thin); + assert(!border->right.border_color); + assert(!border->right.border_width); + assert(!border->diagonal.style); + assert(!border->diagonal.border_color); + assert(!border->diagonal.border_width); + assert(!border->diagonal_bl_tr.style); + assert(!border->diagonal_bl_tr.border_color); + assert(!border->diagonal_bl_tr.border_width); + assert(!border->diagonal_tl_br.style); + assert(!border->diagonal_tl_br.border_color); + assert(!border->diagonal_tl_br.border_width); + break; + case ss::border_direction_t::bottom: + assert(!border->top.style); + assert(!border->top.border_color); + assert(!border->top.border_width); + assert(border->bottom.style); + assert(*border->bottom.style == ss::border_style_t::thin); + assert(!border->bottom.border_color); + assert(!border->bottom.border_width); + assert(!border->left.style); + assert(!border->left.border_color); + assert(!border->left.border_width); + assert(!border->right.style); + assert(!border->right.border_color); + assert(!border->right.border_width); + assert(!border->diagonal.style); + assert(!border->diagonal.border_color); + assert(!border->diagonal.border_width); + assert(!border->diagonal_bl_tr.style); + assert(!border->diagonal_bl_tr.border_color); + assert(!border->diagonal_bl_tr.border_width); + assert(!border->diagonal_tl_br.style); + assert(!border->diagonal_tl_br.border_color); + assert(!border->diagonal_tl_br.border_width); + break; + case spreadsheet::border_direction_t::diagonal: + assert(!border->top.style); + assert(!border->top.border_color); + assert(!border->top.border_width); + assert(!border->bottom.style); + assert(!border->bottom.border_color); + assert(!border->bottom.border_width); + assert(!border->left.style); + assert(!border->left.border_color); + assert(!border->left.border_width); + assert(!border->right.style); + assert(!border->right.border_color); + assert(!border->right.border_width); + assert(!border->diagonal.style); + assert(!border->diagonal.border_color); + assert(!border->diagonal.border_width); + assert(border->diagonal_bl_tr.style); + assert(*border->diagonal_bl_tr.style == ss::border_style_t::thin); + assert(!border->diagonal_bl_tr.border_color); + assert(!border->diagonal_bl_tr.border_width); + assert(border->diagonal_tl_br.style); + assert(*border->diagonal_tl_br.style == ss::border_style_t::thin); + assert(!border->diagonal_tl_br.border_color); + assert(!border->diagonal_tl_br.border_width); + break; + case spreadsheet::border_direction_t::diagonal_tl_br: + assert(!border->top.style); + assert(!border->top.border_color); + assert(!border->top.border_width); + assert(!border->bottom.style); + assert(!border->bottom.border_color); + assert(!border->bottom.border_width); + assert(!border->left.style); + assert(!border->left.border_color); + assert(!border->left.border_width); + assert(!border->right.style); + assert(!border->right.border_color); + assert(!border->right.border_width); + assert(!border->diagonal.style); + assert(!border->diagonal.border_color); + assert(!border->diagonal.border_width); + assert(!border->diagonal_bl_tr.style); + assert(!border->diagonal_bl_tr.border_color); + assert(!border->diagonal_bl_tr.border_width); + assert(border->diagonal_tl_br.style); + assert(*border->diagonal_tl_br.style == ss::border_style_t::thin); + assert(!border->diagonal_tl_br.border_color); + assert(!border->diagonal_tl_br.border_width); + break; + case spreadsheet::border_direction_t::diagonal_bl_tr: + assert(!border->top.style); + assert(!border->top.border_color); + assert(!border->top.border_width); + assert(!border->bottom.style); + assert(!border->bottom.border_color); + assert(!border->bottom.border_width); + assert(!border->left.style); + assert(!border->left.border_color); + assert(!border->left.border_width); + assert(!border->right.style); + assert(!border->right.border_color); + assert(!border->right.border_width); + assert(!border->diagonal.style); + assert(!border->diagonal.border_color); + assert(!border->diagonal.border_width); + assert(border->diagonal_bl_tr.style); + assert(*border->diagonal_bl_tr.style == ss::border_style_t::thin); + assert(!border->diagonal_bl_tr.border_color); + assert(!border->diagonal_bl_tr.border_width); + assert(!border->diagonal_tl_br.style); + assert(!border->diagonal_tl_br.border_color); + assert(!border->diagonal_tl_br.border_width); + break; + default: + assert(!"unhandled direction!"); + } + } +} + +void test_xls_xml_cell_borders_colors() +{ + ORCUS_TEST_FUNC_SCOPE; + + using spreadsheet::color_t; + using spreadsheet::border_style_t; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/borders/colors.xml"); + + spreadsheet::styles& styles = doc->get_styles(); + + spreadsheet::sheet* sh = doc->get_sheet(0); + assert(sh); + + struct check + { + spreadsheet::row_t row; + spreadsheet::col_t col; + color_t color; + }; + + std::vector<check> checks = + { + { 2, 1, color_t(0xFF, 0xFF, 0, 0) }, // B3 - red + { 3, 1, color_t(0xFF, 0, 0x70, 0xC0) }, // B4 - blue + { 4, 1, color_t(0xFF, 0, 0xB0, 0x50) }, // B5 - green + }; + + for (const check& c : checks) + { + size_t xf = sh->get_cell_format(c.row, c.col); // B3 + + const spreadsheet::cell_format_t* cf = styles.get_cell_format(xf); + assert(cf); + assert(cf->apply_border); + + const spreadsheet::border_t* border = styles.get_border(cf->border); + assert(border); + + assert(!border->left.style); + assert(border->right.style); + assert(*border->right.style == border_style_t::thick); + assert(!border->top.style); + assert(!border->bottom.style); + + assert(border->right.border_color == c.color); + } + + // B7 contains yellow left border, purple right border, and light blue + // diagonal borders. + + size_t xf = sh->get_cell_format(6, 1); // B7 + + const spreadsheet::cell_format_t* cf = styles.get_cell_format(xf); + assert(cf); + assert(cf->apply_border); + + const spreadsheet::border_t* border = styles.get_border(cf->border); + assert(border); + + assert(border->left.style == border_style_t::thick); + assert(border->left.border_color == color_t(0xFF, 0xFF, 0xFF, 0)); // yellow + + assert(border->right.style == border_style_t::thick); + assert(border->right.border_color == color_t(0xFF, 0x70, 0x30, 0xA0)); // purple + + assert(border->diagonal_bl_tr.style == border_style_t::thick); + assert(border->diagonal_bl_tr.border_color == color_t(0xFF, 0x00, 0xB0, 0xF0)); // light blue + + assert(border->diagonal_tl_br.style == border_style_t::thick); + assert(border->diagonal_tl_br.border_color == color_t(0xFF, 0x00, 0xB0, 0xF0)); // light blue + + // B7 also contains multi-line string. Test that as well. + ixion::model_context& model = doc->get_model_context(); + ixion::string_id_t sid = model.get_string_identifier(ixion::abs_address_t(0,6,1)); + const std::string* s = model.get_string(sid); + assert(s); + assert(*s == "<- Yellow\nPurple ->\nLight Blue \\"); +} + +void test_xls_xml_hidden_rows_columns() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/hidden-rows-columns/input.xml"); + + spreadsheet::sheet* sh = doc->get_sheet("Hidden Rows"); + assert(sh); + + spreadsheet::row_t row_start = -1, row_end = -1; + + // Row 1 is visible. + assert(!sh->is_row_hidden(0, &row_start, &row_end)); + assert(row_start == 0); + assert(row_end == 1); // the end position is non-inclusive. + + // Rows 2-3 are hidden. + assert(sh->is_row_hidden(1, &row_start, &row_end)); + assert(row_start == 1); + assert(row_end == 3); // the end position is non-inclusive. + + // Row 4 is visible. + assert(!sh->is_row_hidden(3, &row_start, &row_end)); + assert(row_start == 3); + assert(row_end == 4); // the end position is non-inclusive. + + // Row 5 is hidden. + assert(sh->is_row_hidden(4, &row_start, &row_end)); + assert(row_start == 4); + assert(row_end == 5); // the end position is non-inclusive. + + // Rows 6-8 are visible. + assert(!sh->is_row_hidden(5, &row_start, &row_end)); + assert(row_start == 5); + assert(row_end == 8); // the end position is non-inclusive. + + // Row 9 is hidden. + assert(sh->is_row_hidden(8, &row_start, &row_end)); + assert(row_start == 8); + assert(row_end == 9); // the end position is non-inclusive. + + // The rest of the rows are visible. + assert(!sh->is_row_hidden(9, &row_start, &row_end)); + assert(row_start == 9); + assert(row_end == doc->get_sheet_size().rows); // the end position is non-inclusive. + + sh = doc->get_sheet("Hidden Columns"); + assert(sh); + + spreadsheet::col_t col_start = -1, col_end = -1; + + // Columns A-B are visible. + assert(!sh->is_col_hidden(0, &col_start, &col_end)); + assert(col_start == 0); + assert(col_end == 2); // non-inclusive + + // Columns C-E are hidden. + assert(sh->is_col_hidden(2, &col_start, &col_end)); + assert(col_start == 2); + assert(col_end == 6); // non-inclusive + + // Columns G-J are visible. + assert(!sh->is_col_hidden(6, &col_start, &col_end)); + assert(col_start == 6); + assert(col_end == 10); // non-inclusive + + // Column K is hidden. + assert(sh->is_col_hidden(10, &col_start, &col_end)); + assert(col_start == 10); + assert(col_end == 11); // non-inclusive + + // The rest of the columns are all visible. + assert(!sh->is_col_hidden(11, &col_start, &col_end)); + assert(col_start == 11); + assert(col_end == doc->get_sheet_size().columns); // non-inclusive +} + +void test_xls_xml_character_set() +{ + ORCUS_TEST_FUNC_SCOPE; + + doc_loader loader(SRCDIR"/test/xls-xml/character-set/input.xml"); + assert(loader.get_factory().get_character_set() == character_set_t::windows_1252); +} + +void test_xls_xml_number_format() +{ + ORCUS_TEST_FUNC_SCOPE; + + doc_loader loader(SRCDIR"/test/xls-xml/number-format/date-time.xml"); + + const spreadsheet::document& doc = loader.get_doc(); + const spreadsheet::styles& styles = doc.get_styles(); + + const spreadsheet::sheet* sh = doc.get_sheet(0); + assert(sh); + + struct check + { + ss::row_t row; + ss::col_t col; + std::string_view expected; + }; + + std::vector<check> checks = + { + { 1, 1, "[$-F800]dddd\\,\\ mmmm\\ dd\\,\\ yyyy" }, + { 2, 1, "[ENG][$-409]mmmm\\ d\\,\\ yyyy;@" }, + { 3, 1, "m/d/yy;@" }, + { 4, 1, "m/d/yyyy h:mm" }, // General Date + { 5, 1, "d-mmm-yy" }, // Medium Date + { 6, 1, "m/d/yyyy" }, // Short Date + { 7, 1, "h:mm:ss AM/PM" }, // Long Time + { 8, 1, "h:mm AM/PM" }, // Medium Time + { 9, 1, "h:mm" }, // Short Time + { 10, 1, "0.00" }, // Fixed + { 11, 1, "#,##0.00" }, // Standard + { 12, 1, "0.00%" }, // Percent + { 13, 1, "0.00E+00" }, // Scientific + { 14, 1, "\"Yes\";\"Yes\";\"No\"" }, // Yes/No + { 15, 1, "\"True\";\"True\";\"False\"" }, // True/False + { 16, 1, "\"On\";\"On\";\"Off\"" }, // On/Off + { 17, 1, "$#,##0.00_);[Red]($#,##0.00)" }, // Currency + { 18, 1, "[$\xe2\x82\xac-x-euro2] #,##0.00_);[Red]([$\xe2\x82\xac-x-euro2] #,##0.00)" }, // Euro Currency + }; + + for (const check& c : checks) + { + size_t xf = sh->get_cell_format(c.row, c.col); + const spreadsheet::cell_format_t* cf = styles.get_cell_format(xf); + assert(cf); + + const spreadsheet::number_format_t* nf = styles.get_number_format(cf->number_format); + assert(nf); + assert(nf->format_string == c.expected); + } +} + +void test_xls_xml_cell_properties_wrap_and_shrink() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/cell-properties/wrap-and-shrink.xml"); + + const ss::styles& styles = doc->get_styles(); + const ss::sheet* sh = doc->get_sheet(0); + assert(sh); + + std::size_t xfid = sh->get_cell_format(0, 1); // B1 + const ss::cell_format_t* xf = styles.get_cell_format(xfid); + assert(xf); + assert(xf->wrap_text); + assert(!*xf->wrap_text); + assert(xf->shrink_to_fit); + assert(!*xf->shrink_to_fit); + + xfid = sh->get_cell_format(1, 1); // B2 + xf = styles.get_cell_format(xfid); + assert(xf); + assert(xf->wrap_text); + assert(*xf->wrap_text); + assert(xf->shrink_to_fit); + assert(!*xf->shrink_to_fit); + + xfid = sh->get_cell_format(2, 1); // B3 + xf = styles.get_cell_format(xfid); + assert(xf); + assert(xf->wrap_text); + assert(!*xf->wrap_text); + assert(xf->shrink_to_fit); + assert(*xf->shrink_to_fit); +} + +void test_xls_xml_cell_properties_default_style() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/cell-properties/default-style.xml"); + + const ss::color_t black{255, 0, 0, 0}; + + const ss::styles& styles = doc->get_styles(); + const ss::sheet* sh = doc->get_sheet(0); + assert(sh); + + std::size_t xfid_default = sh->get_cell_format(0, 0); // A1 + const ss::cell_format_t* xf = styles.get_cell_format(xfid_default); + assert(xf); + + // alignments + assert(xf->hor_align == ss::hor_alignment_t::center); + assert(xf->ver_align == ss::ver_alignment_t::bottom); + + // font + const ss::font_t* font_style = styles.get_font(xf->font); + assert(font_style); + assert(font_style->name == "DejaVu Sans"); + assert(font_style->size == 12.0); + assert(font_style->color == black); + + // fill + const ss::fill_t* fill_style = styles.get_fill(xf->fill); + assert(fill_style); + assert(fill_style->pattern_type == ss::fill_pattern_t::solid); + const ss::color_t fill_color_fg{255, 0xE2, 0xEF, 0xDA}; + assert(fill_style->fg_color == fill_color_fg); + + // border + const ss::border_t* border_style = styles.get_border(xf->border); + assert(border_style); + + assert(border_style->bottom.style == ss::border_style_t::dotted); + + // number format + const ss::number_format_t* numfmt = styles.get_number_format(xf->number_format); + assert(numfmt); + assert(numfmt->format_string == "0.0000"); + + // protection + const ss::protection_t* prot = styles.get_protection(xf->protection); + assert(prot); + assert(prot->formula_hidden); + + // A1:G6 should all use the default cell style. + for (ss::row_t row = 0; row <= 5; ++row) + { + for (ss::col_t col = 0; col <= 6; ++col) + { + assert(sh->get_cell_format(row, col) == xfid_default); + } + } +} + +void test_xls_xml_cell_properties_locked_and_hidden() +{ + ORCUS_TEST_FUNC_SCOPE; + + auto doc = load_doc_from_filepath(SRCDIR"/test/xls-xml/cell-properties/locked-and-hidden.xml"); + const ixion::model_context& model = doc->get_model_context(); + + const ss::sheet* sh = doc->get_sheet(0); + assert(sh); + + { + // Check cell string values first. + + struct check_type + { + ixion::abs_address_t address; + std::string_view str; + }; + + const check_type checks[] = { + // sheet, row, column, expected cell string value + { { 0, 0, 0 }, "Default (Should be locked but not hidden)" }, + { { 0, 0, 1 }, "Not Locked and not hidden" }, + { { 0, 1, 0 }, "Locked and hidden" }, + { { 0, 1, 1 }, "Not locked and hidden" }, + }; + + for (const auto& c : checks) + { + ixion::string_id_t sid = model.get_string_identifier(c.address); + const std::string* s = model.get_string(sid); + assert(s); + assert(*s == c.str); + } + } + + { + // Check the cell protection attributes. + + struct check_type + { + ss::row_t row; + ss::col_t col; + bool locked; + bool formula_hidden; + }; + + const check_type checks[] = { + // row, column, locked, formula-hidden + { 0, 0, true, false }, + { 0, 1, false, false }, + { 1, 0, true, true }, + { 1, 1, false, true }, + }; + + const ss::styles& styles = doc->get_styles(); + + for (const auto& c : checks) + { + std::cout << "row=" << c.row << "; col=" << c.col << std::endl; + + std::size_t xfid = sh->get_cell_format(c.row, c.col); + const ss::cell_format_t* xf = styles.get_cell_format(xfid); + assert(xf); + + const ss::protection_t* prot = styles.get_protection(xf->protection); + assert(prot); + assert(prot->locked); + assert(prot->formula_hidden); + std::cout << " * locked: expected=" << c.locked << "; actual=" << *prot->locked << std::endl; + assert(*prot->locked == c.locked); + std::cout << " * formula-hidden: expected=" << c.formula_hidden << "; actual=" << *prot->formula_hidden << std::endl; + assert(*prot->formula_hidden == c.formula_hidden); + } + } +} + +void test_xls_xml_styles_direct_format() +{ + ORCUS_TEST_FUNC_SCOPE; + + std::string path{SRCDIR"/test/xls-xml/styles/direct-format.xml"}; + auto doc = load_doc_from_filepath(path, false, ss::formula_error_policy_t::fail); + assert(doc); + + const auto& model = doc->get_model_context(); + + { + // Check cell string values first. + + struct check_type + { + ixion::abs_address_t address; + std::string_view str; + }; + + const check_type checks[] = { + // sheet, row, column, expected cell string value + { { 0, 1, 1 }, "Bold and underlined" }, + { { 0, 3, 1 }, "Yellow background\nand\nright aligned" }, + { { 0, 5, 3 }, "Named Format (Good)" }, + { { 0, 7, 3 }, "Named Format (Good) plus direct format on top" }, + }; + + for (const auto& c : checks) + { + ixion::string_id_t sid = model.get_string_identifier(c.address); + const std::string* s = model.get_string(sid); + assert(s); + assert(*s == c.str); + } + } + + const ss::sheet* sh = doc->get_sheet(0); + assert(sh); + + const ss::styles& styles = doc->get_styles(); + + // Text in B2 is bold, underlined, and horizontally and vertically centered. + auto xfid = sh->get_cell_format(1, 1); + const ss::cell_format_t* xf = styles.get_cell_format(xfid); + assert(xf); + + const ss::font_t* font = styles.get_font(xf->font); + assert(font); + assert(font->bold); + assert(*font->bold); + + const auto* border = styles.get_border(xf->border); + assert(border); + + // "Continuous" with a weight of 1 is mapped to 'thin' border style. + assert(border->bottom.style); + assert(*border->bottom.style == ss::border_style_t::thin); + + assert(xf->hor_align == ss::hor_alignment_t::center); + assert(xf->ver_align == ss::ver_alignment_t::middle); + + // B4 has yellow background, has "Calibri" font at 14 pt etc + xfid = sh->get_cell_format(3, 1); + + xf = styles.get_cell_format(xfid); + assert(xf); + + font = styles.get_font(xf->font); + assert(font); + assert(font->name); + assert(*font->name == "Calibri"); + assert(font->size); + assert(*font->size == 14.0); + assert(font->color); + assert(*font->color == ss::color_t(0xFF, 0x37, 0x56, 0x23)); + + // B4 has yellow background + const ss::fill_t* fill = styles.get_fill(xf->fill); + assert(fill); + assert(fill->pattern_type); + assert(*fill->pattern_type == ss::fill_pattern_t::solid); + assert(fill->fg_color); + assert(*fill->fg_color == ss::color_t(0xFF, 0xFF, 0xFF, 0x00)); + + // B4 is horizontally right-aligned and vertically bottom-aligned + assert(xf->hor_align == ss::hor_alignment_t::right); + assert(xf->ver_align == ss::ver_alignment_t::bottom); + + // B4 has wrap text on + assert(xf->wrap_text && *xf->wrap_text); + + // D6 only uses "Good" named cell style with no direct formatting + xfid = sh->get_cell_format(5, 3); + xf = styles.get_cell_format(xfid); + assert(xf); + + const auto xfid_style_good = xf->style_xf; + const ss::cell_style_t* xstyle = styles.get_cell_style_by_xf(xf->style_xf); + assert(xstyle); + assert(xstyle->name == "Good"); + + // Check the format detail of the "Good" style + xf = styles.get_cell_style_format(xstyle->xf); + assert(xf); + + font = styles.get_font(xf->font); + assert(font); + assert(font->name); + assert(*font->name == "Calibri"); + assert(font->size); + assert(*font->size == 11.0); + assert(font->color); + assert(*font->color == ss::color_t(0xFF, 0x00, 0x61, 0x00)); + + fill = styles.get_fill(xf->fill); + assert(fill); + assert(fill->pattern_type); + assert(*fill->pattern_type == ss::fill_pattern_t::solid); + assert(fill->fg_color); + assert(*fill->fg_color == ss::color_t(0xFF, 0xC6, 0xEF, 0xCE)); + + // D8 has some direct formats applied on top of "Good" named style + xfid = sh->get_cell_format(7, 3); + xf = styles.get_cell_format(xfid); + assert(xf); + + // Make sure it has the "Good" style as its basis + assert(xf->style_xf == xfid_style_good); + xstyle = styles.get_cell_style_by_xf(xf->style_xf); + assert(xstyle); + assert(xstyle->name == "Good"); + + // Format directly applied to D8 on top of "Good" style + assert(xf->hor_align == ss::hor_alignment_t::center); + assert(xf->ver_align == ss::ver_alignment_t::bottom); + assert(xf->wrap_text); + assert(*xf->wrap_text); + font = styles.get_font(xf->font); + assert(font); + assert(font->bold); + assert(*font->bold); +} + +void test_xls_xml_styles_column_styles() +{ + ORCUS_TEST_FUNC_SCOPE; + + std::string path{SRCDIR"/test/xls-xml/styles/column-styles.xml"}; + auto doc = load_doc_from_filepath(path, false, ss::formula_error_policy_t::fail); + assert(doc); + auto doc_size = doc->get_sheet_size(); + + const ss::styles& styles = doc->get_styles(); + + const ss::sheet* sh = doc->get_sheet(0); + assert(sh); + + { + // On Sheet1, check the named styles applied on columns B:D and F. + // Columns A and E should have Normal style applied. + const std::tuple<ss::row_t, ss::col_t, std::string> checks[] = { + { 0, 0, "Normal" }, + { 0, 1, "Bad" }, + { 0, 2, "Good" }, + { 0, 3, "Neutral" }, + { 0, 4, "Normal" }, + { 0, 5, "Note" }, + { doc_size.rows - 1, 0, "Normal" }, + { doc_size.rows - 1, 1, "Bad" }, + { doc_size.rows - 1, 2, "Good" }, + { doc_size.rows - 1, 3, "Neutral" }, + { doc_size.rows - 1, 4, "Normal" }, + { doc_size.rows - 1, 5, "Note" }, + }; + + for (const auto& check : checks) + { + ss::row_t r = std::get<0>(check); + ss::col_t c = std::get<1>(check); + std::string_view name = std::get<2>(check); + + std::size_t xfid = sh->get_cell_format(r, c); + std::cout << "row=" << r << "; column=" << c << "; xfid=" << xfid << std::endl; + const ss::cell_format_t* xf = styles.get_cell_format(xfid); + assert(xf); + std::cout << "style xfid=" << xf->style_xf << std::endl; + + const ss::cell_style_t* xstyle = styles.get_cell_style_by_xf(xf->style_xf); + assert(xstyle); + if (xstyle->name != name) + std::cout << "names differ! (expected=" << name << "; actual=" << xstyle->name << ")" << std::endl; + + assert(xstyle->name == name); + } + } + + { + // Row 10 has green background, and row 11 has orange background. + const std::tuple<ss::row_t, ss::color_t> checks[] = { + { 9, {0xFF, 0x92, 0xD0, 0x50} }, + { 10, {0xFF, 0xFF, 0xC0, 0x00} }, + }; + + for (const auto& check : checks) + { + const ss::row_t row = std::get<0>(check); + const ss::color_t color = std::get<1>(check); + + for (ss::col_t col = 0; col <= 6; ++col) + { + std::size_t xfid = sh->get_cell_format(row, col); + std::cout << "row=" << row << "; column=" << col << "; xfid=" << xfid << std::endl; + const ss::cell_format_t* xf = styles.get_cell_format(xfid); + assert(xf); + + const ss::fill_t* fill = styles.get_fill(xf->fill); + assert(fill); + + assert(fill->pattern_type); + assert(*fill->pattern_type == ss::fill_pattern_t::solid); + + assert(fill->fg_color); + assert(*fill->fg_color == color); + } + } + } + + sh = doc->get_sheet(1); + assert(sh); + + // Columns B:D should have "Good" named style applied. + { + const std::pair<ss::row_t, ss::col_t> cells[] = { + { 0, 1 }, + { 0, 3 }, + { doc_size.rows - 1, 1 }, + { doc_size.rows - 1, 3 }, + }; + + for (const auto& cell : cells) + { + std::size_t xfid = sh->get_cell_format(cell.first, cell.second); + const ss::cell_format_t* xf = styles.get_cell_format(xfid); + assert(xf); + + const ss::cell_style_t* xstyle = styles.get_cell_style_by_xf(xf->style_xf); + assert(xstyle); + assert(xstyle->name == "Good"); + } + } +} + +void test_xls_xml_styles_data_offset() +{ + ORCUS_TEST_FUNC_SCOPE; + + std::string path{SRCDIR"/test/xls-xml/styles/data-offset.xml"}; + auto doc = load_doc_from_filepath(path, false, ss::formula_error_policy_t::fail); + assert(doc); + + const ss::styles& styles = doc->get_styles(); + + const ss::sheet* sh = doc->get_sheet(0); + assert(sh); + + auto check_font_style1 = [sh, &styles](ss::row_t row, ss::col_t col) + { + std::size_t xfid = sh->get_cell_format(row, col); + const ss::cell_format_t* xf = styles.get_cell_format(xfid); + assert(xf); + + const ss::font_t* font = styles.get_font(xf->font); + assert(font); + assert(font->bold); + assert(font->color); + assert(*font->color == ss::color_t(0xFF, 0x00, 0x80, 0x00)); + const ss::number_format_t* numfmt = styles.get_number_format(xf->number_format); + assert(numfmt); + assert(numfmt->format_string); + assert(*numfmt->format_string == "0.00_ ;[Red]\\-0.00\\ "); + }; + + auto check_font_style2 = [sh, &styles](ss::row_t row, ss::col_t col) + { + const ss::color_t red{0xFF, 0xFF, 0x00, 0x00}; + + std::size_t xfid = sh->get_cell_format(row, col); + const ss::cell_format_t* xf = styles.get_cell_format(xfid); + assert(xf); + + const ss::font_t* font = styles.get_font(xf->font); + assert(font); + assert(font->color); + assert(*font->color == red); + }; + + auto check_font_style3 = [sh, &styles](ss::row_t row, ss::col_t col) + { + const ss::color_t blue{0xFF, 0x00, 0x00, 0xFF}; + + std::size_t xfid = sh->get_cell_format(row, col); + const ss::cell_format_t* xf = styles.get_cell_format(xfid); + assert(xf); + + const ss::font_t* font = styles.get_font(xf->font); + assert(font); + assert(font->color); + assert(*font->color == blue); + + const ss::number_format_t* numfmt = styles.get_number_format(xf->number_format); + assert(numfmt); + assert(numfmt->format_string); + assert(*numfmt->format_string == "yyyy/mm\\-dd;@"); + }; + + // Column B and row 2 should have font with bold, green-ish with number format applied + check_font_style1(0, 1); + check_font_style1(1, 1); + check_font_style1(2, 1); + check_font_style1(3, 1); + + // row 2 + check_font_style1(1, 0); + check_font_style1(1, 1); + check_font_style1(1, 2); + check_font_style1(1, 3); + + // Column C should have red font (except for row 2) + check_font_style2(0, 2); + check_font_style2(2, 2); + check_font_style2(3, 2); + + // Column D should have blue font with custom number format applied (except for row 2) + check_font_style3(0, 3); + check_font_style3(2, 3); + check_font_style3(3, 3); +} + +void test_xls_xml_view_cursor_per_sheet() +{ + ORCUS_TEST_FUNC_SCOPE; + + std::string path(SRCDIR"/test/xls-xml/view/cursor-per-sheet.xml"); + + spreadsheet::range_size_t ss{1048576, 16384}; + spreadsheet::document doc{ss}; + spreadsheet::view view(doc); + spreadsheet::import_factory factory(doc, view); + orcus_xls_xml app(&factory); + app.set_config(test_config); + + app.read_file(path.c_str()); + + // Sheet3 should be active. + assert(view.get_active_sheet() == 2); + + const spreadsheet::sheet_view* sv = view.get_sheet_view(0); + assert(sv); + + // NB : the resolver type is set to R1C1 for Excel XML 2003. + spreadsheet::iface::import_reference_resolver* resolver = + factory.get_reference_resolver(spreadsheet::formula_ref_context_t::global); + assert(resolver); + + // On Sheet1, the cursor should be set to C4. + spreadsheet::range_t expected = to_rc_range(resolver->resolve_range("R4C3")); + spreadsheet::range_t actual = sv->get_selection(spreadsheet::sheet_pane_t::top_left); + assert(expected == actual); + + sv = view.get_sheet_view(1); + assert(sv); + + // On Sheet2, the cursor should be set to D8. + expected = to_rc_range(resolver->resolve_range("R8C4")); + actual = sv->get_selection(spreadsheet::sheet_pane_t::top_left); + assert(expected == actual); + + sv = view.get_sheet_view(2); + assert(sv); + + // On Sheet3, the cursor should be set to D2. + expected = to_rc_range(resolver->resolve_range("R2C4")); + actual = sv->get_selection(spreadsheet::sheet_pane_t::top_left); + assert(expected == actual); + + sv = view.get_sheet_view(3); + assert(sv); + + // On Sheet4, the cursor should be set to C5:E8. + expected = to_rc_range(resolver->resolve_range("R5C3:R8C5")); + actual = sv->get_selection(spreadsheet::sheet_pane_t::top_left); + assert(expected == actual); +} + +struct expected_selection +{ + spreadsheet::sheet_pane_t pane; + std::string_view sel; +}; + +void test_xls_xml_view_cursor_split_pane() +{ + ORCUS_TEST_FUNC_SCOPE; + + std::string path(SRCDIR"/test/xls-xml/view/cursor-split-pane.xml"); + + spreadsheet::range_size_t ss{1048576, 16384}; + spreadsheet::document doc{ss}; + spreadsheet::view view(doc); + spreadsheet::import_factory factory(doc, view); + orcus_xls_xml app(&factory); + app.set_config(test_config); + + app.read_file(path.c_str()); + + // NB : the resolver type is set to R1C1 for Excel XML 2003. + spreadsheet::iface::import_reference_resolver* resolver = + factory.get_reference_resolver(spreadsheet::formula_ref_context_t::global); + assert(resolver); + + // Sheet4 should be active. + assert(view.get_active_sheet() == 3); + + const spreadsheet::sheet_view* sv = view.get_sheet_view(0); + assert(sv); + + // On Sheet1, the view is split into 4. + assert(sv->get_active_pane() == spreadsheet::sheet_pane_t::bottom_left); + assert(sv->get_split_pane().hor_split == 5190.0); + assert(sv->get_split_pane().ver_split == 1800.0); + + { + spreadsheet::address_t expected = to_rc_address(resolver->resolve_address("R6C6")); + spreadsheet::address_t actual = sv->get_split_pane().top_left_cell; + assert(expected == actual); + } + + std::vector<expected_selection> expected_selections = + { + { spreadsheet::sheet_pane_t::top_left, "R4C5" }, + { spreadsheet::sheet_pane_t::top_right, "R2C10" }, + { spreadsheet::sheet_pane_t::bottom_left, "R8C1" }, + { spreadsheet::sheet_pane_t::bottom_right, "R17C10" }, + }; + + for (const expected_selection& es : expected_selections) + { + // cursor in the top-left pane. + spreadsheet::range_t expected = to_rc_range(resolver->resolve_range(es.sel)); + spreadsheet::range_t actual = sv->get_selection(es.pane); + assert(expected == actual); + } + + sv = view.get_sheet_view(1); + assert(sv); + + // Sheet2 is also split into 4 views. + assert(sv->get_active_pane() == spreadsheet::sheet_pane_t::top_right); + assert(sv->get_split_pane().hor_split == 5190.0); + assert(sv->get_split_pane().ver_split == 2400.0); + + { + spreadsheet::address_t expected = to_rc_address(resolver->resolve_address("R8C6")); + spreadsheet::address_t actual = sv->get_split_pane().top_left_cell; + assert(expected == actual); + } + + expected_selections = + { + { spreadsheet::sheet_pane_t::top_left, "R2C3:R6C3" }, + { spreadsheet::sheet_pane_t::top_right, "R2C8:R2C12" }, + { spreadsheet::sheet_pane_t::bottom_left, "R18C2:R23C3" }, + { spreadsheet::sheet_pane_t::bottom_right, "R11C8:R13C10" }, + }; + + for (const expected_selection& es : expected_selections) + { + // cursor in the top-left pane. + spreadsheet::range_t expected = to_rc_range(resolver->resolve_range(es.sel)); + spreadsheet::range_t actual = sv->get_selection(es.pane); + assert(expected == actual); + } + + sv = view.get_sheet_view(2); + assert(sv); + + // Sheet3 is horizontally split into top and bottom views (top-left and bottom-left). + assert(sv->get_active_pane() == spreadsheet::sheet_pane_t::bottom_left); + assert(sv->get_split_pane().hor_split == 0.0); + assert(sv->get_split_pane().ver_split == 1500.0); + + { + spreadsheet::address_t expected = to_rc_address(resolver->resolve_address("R5C1")); + spreadsheet::address_t actual = sv->get_split_pane().top_left_cell; + assert(expected == actual); + } + + expected_selections = + { + { spreadsheet::sheet_pane_t::top_left, "R2C4" }, + { spreadsheet::sheet_pane_t::bottom_left, "R9C3" }, + }; + + for (const expected_selection& es : expected_selections) + { + // cursor in the top-left pane. + spreadsheet::range_t expected = to_rc_range(resolver->resolve_range(es.sel)); + spreadsheet::range_t actual = sv->get_selection(es.pane); + assert(expected == actual); + } + + sv = view.get_sheet_view(3); + assert(sv); + + // Sheet4 is vertically split into left and right views (top-left and top-right). + assert(sv->get_active_pane() == spreadsheet::sheet_pane_t::top_left); + assert(sv->get_split_pane().hor_split == 4230.0); + assert(sv->get_split_pane().ver_split == 0.0); + + { + spreadsheet::address_t expected = to_rc_address(resolver->resolve_address("R1C5")); + spreadsheet::address_t actual = sv->get_split_pane().top_left_cell; + assert(expected == actual); + } + + expected_selections = + { + { spreadsheet::sheet_pane_t::top_left, "R18C2" }, + { spreadsheet::sheet_pane_t::top_right, "R11C9" }, + }; + + for (const expected_selection& es : expected_selections) + { + // cursor in the top-left pane. + spreadsheet::range_t expected = to_rc_range(resolver->resolve_range(es.sel)); + spreadsheet::range_t actual = sv->get_selection(es.pane); + assert(expected == actual); + } +} + +void test_xls_xml_view_frozen_pane() +{ + ORCUS_TEST_FUNC_SCOPE; + + std::string path(SRCDIR"/test/xls-xml/view/frozen-pane.xml"); + + spreadsheet::range_size_t ss{1048576, 16384}; + spreadsheet::document doc{ss}; + spreadsheet::view view(doc); + spreadsheet::import_factory factory(doc, view); + orcus_xls_xml app(&factory); + app.set_config(test_config); + + app.read_file(path.c_str()); + + // NB : the resolver type is set to R1C1 for Excel XML 2003. + spreadsheet::iface::import_reference_resolver* resolver = + factory.get_reference_resolver(spreadsheet::formula_ref_context_t::global); + assert(resolver); + + // Sheet3 should be active. + assert(view.get_active_sheet() == 2); + + const spreadsheet::sheet_view* sv = view.get_sheet_view(0); + assert(sv); + + { + // Sheet1 is vertically frozen between columns A and B. + const spreadsheet::frozen_pane_t& fp = sv->get_frozen_pane(); + assert(fp.top_left_cell == to_rc_address(resolver->resolve_address("R1C2"))); + assert(fp.visible_columns == 1); + assert(fp.visible_rows == 0); + assert(sv->get_active_pane() == spreadsheet::sheet_pane_t::top_right); + } + + sv = view.get_sheet_view(1); + assert(sv); + + { + // Sheet2 is horizontally frozen between rows 1 and 2. + const spreadsheet::frozen_pane_t& fp = sv->get_frozen_pane(); + assert(fp.top_left_cell == to_rc_address(resolver->resolve_address("R2C1"))); + assert(fp.visible_columns == 0); + assert(fp.visible_rows == 1); + assert(sv->get_active_pane() == spreadsheet::sheet_pane_t::bottom_left); + } + + sv = view.get_sheet_view(2); + assert(sv); + + { + // Sheet3 is frozen both horizontally and vertically. + const spreadsheet::frozen_pane_t& fp = sv->get_frozen_pane(); + assert(fp.top_left_cell == to_rc_address(resolver->resolve_address("R9C5"))); + assert(fp.visible_columns == 4); + assert(fp.visible_rows == 8); + assert(sv->get_active_pane() == spreadsheet::sheet_pane_t::bottom_right); + } +} + +void test_xls_xml_skip_error_cells() +{ + ORCUS_TEST_FUNC_SCOPE; + + std::string path(SRCDIR"/test/xls-xml/formula-cells-parse-error/input.xml"); + + try + { + auto doc = load_doc_from_filepath(path, false, ss::formula_error_policy_t::fail); + (void)doc; + assert(!"exception was expected, but was not thrown."); + } + catch (const std::exception&) + { + // works as expected + } + + auto doc = load_doc_from_filepath(path, false, ss::formula_error_policy_t::skip); + const ixion::model_context& cxt = doc->get_model_context(); + + auto is_formula_cell_with_error = [&cxt](const ixion::abs_address_t& pos) -> bool + { + const ixion::formula_cell* fc = cxt.get_formula_cell(pos); + if (!fc) + return false; + + const ixion::formula_tokens_t& tokens = fc->get_tokens()->get(); + if (tokens.empty()) + return false; + + return tokens[0].opcode == ixion::fop_error; + }; + + // Make sure these two cells have been imported as formula cells with error. + assert(is_formula_cell_with_error(ixion::abs_address_t(0, 1, 1))); + assert(is_formula_cell_with_error(ixion::abs_address_t(0, 4, 0))); +} + +/** + * The test input file starts with double BOM's. + */ +void test_xls_xml_double_bom() +{ + ORCUS_TEST_FUNC_SCOPE; + + std::string path(SRCDIR"/test/xls-xml/double-bom/input.xml"); + + // Make sure the test file does contain double BOM's. + orcus::file_content content{path}; + auto content_s = content.str(); + + constexpr std::string_view BOM = "\xef\xbb\xbf"; + assert(content_s.substr(0, 3) == BOM); + assert(content_s.substr(3, 3) == BOM); + + auto doc = load_doc_from_filepath(path, false, ss::formula_error_policy_t::fail); + + assert(doc->get_sheet_count() == 5u); + assert(doc->get_sheet_name(0) == "Holdings"); + assert(doc->get_sheet_name(1) == "Overview"); + assert(doc->get_sheet_name(2) == "Historical"); + assert(doc->get_sheet_name(3) == "Performance"); + assert(doc->get_sheet_name(4) == "Distributions"); +} + +} // anonymous namespace + +int main() +{ + test_config.debug = false; + test_config.structure_check = true; + + test_xls_xml_detection(); + test_xls_xml_create_filter(); + test_xls_xml_import(); + test_xls_xml_merged_cells(); + test_xls_xml_date_time(); + test_xls_xml_bold_and_italic(); + test_xls_xml_colored_text(); + test_xls_xml_formatted_text_basic(); + test_xls_xml_column_width_row_height(); + test_xls_xml_background_fill(); + test_xls_xml_named_colors(); + test_xls_xml_text_alignment(); + test_xls_xml_cell_borders_single_cells(); + test_xls_xml_cell_borders_directions(); + test_xls_xml_cell_borders_colors(); + test_xls_xml_hidden_rows_columns(); + test_xls_xml_character_set(); + test_xls_xml_number_format(); + test_xls_xml_cell_properties_wrap_and_shrink(); + test_xls_xml_cell_properties_default_style(); + test_xls_xml_cell_properties_locked_and_hidden(); + test_xls_xml_styles_direct_format(); + test_xls_xml_styles_column_styles(); + test_xls_xml_styles_data_offset(); + + // view import + test_xls_xml_view_cursor_per_sheet(); + test_xls_xml_view_cursor_split_pane(); + test_xls_xml_view_frozen_pane(); + + test_xls_xml_skip_error_cells(); + test_xls_xml_double_bom(); + + return EXIT_SUCCESS; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ + |