Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- **Breaking**: `ValueType` gains `boolean`, `date`, `time` and `error`, and
the odf and xlsx readers report them; a boolean states 1 or 0 as its number.
`Sheet::set_cell` writes a boolean and refuses the other three. The enum
mirrors in every binding follow.

- `Text::set_style` and the edit op `setTextStyle {id, style}` write bold,
italic, underline, strikethrough, highlight, colour and size onto a run of
an odf, docx or pptx document. The other engines refuse it.
Expand Down
4 changes: 4 additions & 0 deletions apple/include/OdrCoreObjC/ODRDocumentElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ typedef NS_ENUM(NSInteger, ODRValueType) {
ODRValueTypeUnknown = 0,
ODRValueTypeString,
ODRValueTypeFloatNumber,
ODRValueTypeBoolean,
ODRValueTypeDate,
ODRValueTypeTime,
ODRValueTypeError,
} NS_SWIFT_NAME(ValueType);

/// A node in a document's element tree — `odr::Element`.
Expand Down
4 changes: 4 additions & 0 deletions apple/src/ODRDocumentElement.mm
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@
ODR_SAME_ENUM(ODRValueTypeUnknown, odr::ValueType::unknown);
ODR_SAME_ENUM(ODRValueTypeString, odr::ValueType::string);
ODR_SAME_ENUM(ODRValueTypeFloatNumber, odr::ValueType::float_number);
ODR_SAME_ENUM(ODRValueTypeBoolean, odr::ValueType::boolean);
ODR_SAME_ENUM(ODRValueTypeDate, odr::ValueType::date);
ODR_SAME_ENUM(ODRValueTypeTime, odr::ValueType::time);
ODR_SAME_ENUM(ODRValueTypeError, odr::ValueType::error);

namespace {

Expand Down
2 changes: 1 addition & 1 deletion docs/design/spreadsheet-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ results go stale the moment an input changes.
| XLSX edit | `sheet_set_cell` | Writes a cell value (step 0.2, landed); `text_set_content` is still a no-op |
| XLSX save | `ooxml_spreadsheet_document.cpp::save` | Writes back the worksheets and `workbook.xml`, copies the rest (step 0.2, landed) |
| XLSX cells | `Sheet.cells` `(col,row) → {node, id}` map | Off-tree; a position the file states no `<c>` for is written by `insert_cell` (step 2.2, landed) |
| Cell value | `SheetCellAdapter` | `sheet_cell_value` reads the number and the formula (step 0.1, landed); `sheet_cell_value_type` stays the cheap question the renderer asks. Dates, booleans and errors still report `string` |
| Cell value | `SheetCellAdapter` | `sheet_cell_value` reads the number and the formula (step 0.1, landed); `sheet_cell_value_type` stays the cheap question the renderer asks. A boolean, a date, a time and an error report their own kind; an xlsx date is a `float_number` serial until number formats are read |
| Number formats | — | Not parsed in either engine. ODS shows the producer's cached `text:p`; XLSX shows the raw `<v>` (a date is its serial) |
| Formulas | `sheet_cell_value` | The expression is read and handed out as a string (step 0.1, landed); nothing parses or evaluates it. XLSX shows the cached `<v>`, ODS the cached `text:p`. `xls` and `numbers` drop the expression at parse time |
| Browser: sheet script | `html/frontend/spreadsheet.js` | Hover/pin, raise a clipped cell over its neighbours, sort rows in the DOM. Sorting reorders `<tr>`s, so a row's identity is its `<th>` label, not its index. Publishes `odr.sheet` (step 1.1, landed), and the value and reflow half of it (steps 1.2/1.3, landed) |
Expand Down
2 changes: 1 addition & 1 deletion jni/java/app/opendocument/core/ValueType.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/** Mirrors {@code odr::ValueType}; constant order must match the C++ declaration. */
public enum ValueType {
UNKNOWN, STRING, FLOAT_NUMBER;
UNKNOWN, STRING, FLOAT_NUMBER, BOOLEAN, DATE, TIME, ERROR;

static ValueType fromNative(int code) {
return code < 0 ? null : values()[code];
Expand Down
6 changes: 5 additions & 1 deletion python/src/bind_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ void odr_python::bind_document(py::module_ &m) {
py::enum_<odr::ValueType>(m, "ValueType")
.value("unknown", odr::ValueType::unknown)
.value("string", odr::ValueType::string)
.value("float_number", odr::ValueType::float_number);
.value("float_number", odr::ValueType::float_number)
.value("boolean", odr::ValueType::boolean)
.value("date", odr::ValueType::date)
.value("time", odr::ValueType::time)
.value("error", odr::ValueType::error);

py::class_<odr::TableDimensions>(m, "TableDimensions")
.def(py::init<>())
Expand Down
8 changes: 7 additions & 1 deletion src/odr/document_element.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,17 @@ enum class AnchorType {
at_paragraph,
};

/// Collection of value types.
/// What a cell states, as the file types it. A percentage and a currency are
/// a `string` stating a number until number formats are read, and so is an
/// ooxml date, which is a serial its format shows.
enum class ValueType {
unknown,
string,
float_number,
boolean, ///< states 1 or 0 as its number
date,
time,
error, ///< an evaluation error the file recorded
};

/// @brief What a sheet cell holds: what @ref SheetCell::value reads out of one,
Expand Down
72 changes: 58 additions & 14 deletions src/odr/internal/odf/odf_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,41 @@ void remove_value_attributes(pugi::xml_node node) {
}
}

/// [ODF 1.2] 19.385 `office:value-type`. A percentage and a currency stay a
/// string until number formats are read, since only their text shows them.
ValueType value_type_of(const pugi::xml_node node) {
const char *value_type = node.attribute("office:value-type").value();
if (std::strcmp("float", value_type) == 0) {
return ValueType::float_number;
}
if (std::strcmp("boolean", value_type) == 0) {
return ValueType::boolean;
}
if (std::strcmp("date", value_type) == 0) {
return ValueType::date;
}
if (std::strcmp("time", value_type) == 0) {
return ValueType::time;
}
return ValueType::string;
}

/// Whether the engine has a form to write @p value in.
bool writable(const CellValue &value) {
switch (value.type()) {
case ValueType::unknown:
case ValueType::string:
case ValueType::float_number:
case ValueType::boolean:
return true;
case ValueType::date:
case ValueType::time:
case ValueType::error:
return false;
}
return false;
}

using TreeEditor = xml::TreeEditor<ElementRegistry>;
using xml::NodeSpan;

Expand Down Expand Up @@ -456,6 +491,9 @@ class ElementAdapter final : public AdapterBase {
void sheet_set_cell(const ElementIdentifier element_id,
const std::uint32_t column, const std::uint32_t row,
const CellValue &value) const override {
if (!writable(value)) {
throw UnsupportedOperation();
}
const ElementRegistry::Sheet::Cell *cell =
m_registry->sheet_element_at(element_id).cell(column, row);

Expand Down Expand Up @@ -497,6 +535,16 @@ class ElementAdapter final : public AdapterBase {
node.append_attribute("office:value")
.set_value(fmt::format("{}", value.number()).c_str());
break;
case ValueType::boolean:
node.append_attribute("office:value-type").set_value("boolean");
node.append_attribute("office:boolean-value")
.set_value(value.has_number() && value.number() != 0 ? "true"
: "false");
break;
case ValueType::date:
case ValueType::time:
case ValueType::error:
throw UnsupportedOperation(); // `writable` refused these above
}

drop_stale_results(element_id, column, row);
Expand Down Expand Up @@ -632,15 +680,10 @@ class ElementAdapter final : public AdapterBase {
}
[[nodiscard]] ValueType
sheet_cell_value_type(const ElementIdentifier element_id) const override {
const pugi::xml_node node = get_node(element_id);
if (const char *value_type = node.attribute("office:value-type").value();
std::strcmp("float", value_type) == 0) {
return ValueType::float_number;
}
return ValueType::string;
return value_type_of(get_node(element_id));
}
/// [ODF 1.2] 19.386 `office:value`, 19.642 `table:formula`. A date, a time
/// and a boolean state their value elsewhere and are read as their text.
/// [ODF 1.2] 19.386 `office:value`, 19.642 `table:formula`. A date and a
/// time state their value as text; a boolean states 1 or 0.
[[nodiscard]] CellValue
sheet_cell_value(const ElementIdentifier element_id) const override {
const pugi::xml_node node = get_node(element_id);
Expand All @@ -650,6 +693,12 @@ class ElementAdapter final : public AdapterBase {
util::number::parse(node.attribute("office:value").value())) {
result = result.with_number(*number);
}
if (result.type() == ValueType::boolean) {
const bool set =
std::strcmp("true", node.attribute("office:boolean-value").value()) ==
0;
result = result.with_number(set ? 1 : 0);
}
if (const pugi::xml_attribute formula = node.attribute("table:formula")) {
result = result.with_formula(formula.value());
}
Expand Down Expand Up @@ -893,12 +942,7 @@ class ElementAdapter final : public AdapterBase {
}
[[nodiscard]] ValueType
table_cell_value_type(const ElementIdentifier element_id) const override {
const pugi::xml_node node = get_node(element_id);
if (const char *value_type = node.attribute("office:value-type").value();
std::strcmp("float", value_type) == 0) {
return ValueType::float_number;
}
return ValueType::string;
return value_type_of(get_node(element_id));
}
[[nodiscard]] TableCellStyle
table_cell_style(const ElementIdentifier element_id) const override {
Expand Down
32 changes: 29 additions & 3 deletions src/odr/internal/ooxml/spreadsheet/ooxml_spreadsheet_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ class ElementAdapter final : public AdapterBase {
void sheet_set_cell(const ElementIdentifier element_id,
const std::uint32_t column, const std::uint32_t row,
const CellValue &value) const override {
if (value.type() == ValueType::date || value.type() == ValueType::time ||
value.type() == ValueType::error) {
throw UnsupportedOperation(); // no form to write them in yet
}
const ElementRegistry::Sheet &sheet =
m_registry->sheet_element_at(element_id);
const ElementRegistry::Sheet::Cell *cell = sheet.cell(column, row);
Expand Down Expand Up @@ -288,6 +292,19 @@ class ElementAdapter final : public AdapterBase {
m_registry->create_text_element(value_node, value_node);
m_registry->append_child(cell_id, text_id);
} break;
case ValueType::boolean: {
node.append_attribute("t").set_value("b");
const pugi::xml_node value_node = node.append_child("v");
value_node.text().set(value.has_number() && value.number() != 0 ? "1"
: "0");
const auto &[text_id, unused1, unused2] =
m_registry->create_text_element(value_node, value_node);
m_registry->append_child(cell_id, text_id);
} break;
case ValueType::date:
case ValueType::time:
case ValueType::error:
throw UnsupportedOperation(); // refused above
}
}

Expand Down Expand Up @@ -358,8 +375,16 @@ class ElementAdapter final : public AdapterBase {
// inline ("inlineStr"), or formula ("str") cells.
const pugi::xml_node node = get_node(element_id);
const std::string type = node.attribute("t").value();
if (type == "s" || type == "str" || type == "inlineStr" || type == "b" ||
type == "e" || type == "d") {
if (type == "b") {
return ValueType::boolean;
}
if (type == "e") {
return ValueType::error;
}
if (type == "d") {
return ValueType::date;
}
if (type == "s" || type == "str" || type == "inlineStr") {
return ValueType::string;
}
if (node.child("v")) {
Expand All @@ -374,7 +399,8 @@ class ElementAdapter final : public AdapterBase {
const pugi::xml_node node = get_node(element_id);

CellValue result = CellValue(sheet_cell_value_type(element_id));
if (result.type() == ValueType::float_number) {
if (result.type() == ValueType::float_number ||
result.type() == ValueType::boolean) {
if (const std::optional<double> number =
util::number::parse(node.child("v").text().get())) {
result = result.with_number(*number);
Expand Down
4 changes: 4 additions & 0 deletions test/src/enum_ordinals_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@ TEST(EnumOrdinals, value_type) {
EXPECT_EQ(ordinal(ValueType::unknown), 0);
EXPECT_EQ(ordinal(ValueType::string), 1);
EXPECT_EQ(ordinal(ValueType::float_number), 2);
EXPECT_EQ(ordinal(ValueType::boolean), 3);
EXPECT_EQ(ordinal(ValueType::date), 4);
EXPECT_EQ(ordinal(ValueType::time), 5);
EXPECT_EQ(ordinal(ValueType::error), 6);
}

TEST(EnumOrdinals, list_type) {
Expand Down
26 changes: 26 additions & 0 deletions test/src/internal/odf/odf_sheet_value_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,29 @@ TEST(OdfSheetValue, a_number_is_read_in_one_spelling_only) {

EXPECT_FALSE(value.has_number());
}

/// [ODF 1.2] 19.385: a boolean states `office:boolean-value`, read as 1 or 0.
TEST(OdfSheetValue, a_boolean_cell_is_typed_and_states_one_or_zero) {
const CellValue value = value_of(
R"(<table:table-cell office:value-type="boolean" office:boolean-value="true">)"
R"(<text:p>TRUE</text:p></table:table-cell>)");

EXPECT_EQ(value.type(), ValueType::boolean);
ASSERT_TRUE(value.has_number());
EXPECT_DOUBLE_EQ(value.number(), 1);
}

/// A date and a time state their value as text, which is what the type says.
TEST(OdfSheetValue, a_date_and_a_time_cell_are_typed_and_state_no_number) {
const CellValue date = value_of(
R"(<table:table-cell office:value-type="date" office:date-value="2024-01-31">)"
R"(<text:p>31.01.2024</text:p></table:table-cell>)");
EXPECT_EQ(date.type(), ValueType::date);
EXPECT_FALSE(date.has_number());

const CellValue time = value_of(
R"(<table:table-cell office:value-type="time" office:time-value="PT10H30M00S">)"
R"(<text:p>10:30</text:p></table:table-cell>)");
EXPECT_EQ(time.type(), ValueType::time);
EXPECT_FALSE(time.has_number());
}
27 changes: 27 additions & 0 deletions test/src/internal/odf/odf_sheet_write_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,30 @@ TEST(OdfSheetWrite, a_number_stating_none_leaves_the_cell_alone) {
ValueNotStated);
EXPECT_EQ(sheet.cell(0, 0).value().text(), "old");
}

TEST(OdfSheetWrite, a_boolean_is_written_as_its_value_and_text) {
const Document document = document_of(flat_sheet(string_cell("old")));
const Sheet sheet = first_sheet(document);

sheet.set_cell(
0, 0, CellValue(ValueType::boolean).with_number(1).with_text("TRUE"));

const CellValue value = sheet.cell(0, 0).value();
EXPECT_EQ(value.type(), ValueType::boolean);
ASSERT_TRUE(value.has_number());
EXPECT_DOUBLE_EQ(value.number(), 1);
EXPECT_EQ(value.text(), "TRUE");
}

/// No form to write these in yet, and the refusal leaves the cell as it was.
TEST(OdfSheetWrite, a_date_a_time_and_an_error_refuse_to_be_written) {
const Document document = document_of(flat_sheet(string_cell("old")));
const Sheet sheet = first_sheet(document);

for (const ValueType type :
{ValueType::date, ValueType::time, ValueType::error}) {
EXPECT_THROW(sheet.set_cell(0, 0, CellValue(type).with_text("x")),
UnsupportedOperation);
}
EXPECT_EQ(sheet.cell(0, 0).value().text(), "old");
}
23 changes: 19 additions & 4 deletions test/src/internal/ooxml/ooxml_spreadsheet_value_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,26 @@ TEST(OoxmlSpreadsheetValue, a_shared_formula_that_does_not_parse_is_kept) {
EXPECT_EQ(formula_at(data, 2, 1), "A1 +");
}

/// `<v>` holds `1`, not a quantity, and `c/@t="b"` types the cell a string.
TEST(OoxmlSpreadsheetValue, a_boolean_cell_states_no_number) {
/// ECMA-376 18.18.11: `c/@t="b"` types the cell a boolean, `<v>` its 1 or 0.
TEST(OoxmlSpreadsheetValue, a_boolean_cell_is_typed_and_states_one_or_zero) {
const CellValue value =
value_of(R"(<row r="1"><c r="A1" t="b"><v>1</v></c></row>)");

EXPECT_EQ(value.type(), ValueType::string);
EXPECT_FALSE(value.has_number());
EXPECT_EQ(value.type(), ValueType::boolean);
ASSERT_TRUE(value.has_number());
EXPECT_DOUBLE_EQ(value.number(), 1);
}

/// An error cell states its text and a date cell its ISO 8601 text; neither
/// is a number.
TEST(OoxmlSpreadsheetValue, an_error_and_a_date_cell_are_typed) {
const CellValue error =
value_of(R"(<row r="1"><c r="A1" t="e"><v>#DIV/0!</v></c></row>)");
EXPECT_EQ(error.type(), ValueType::error);
EXPECT_FALSE(error.has_number());

const CellValue date =
value_of(R"(<row r="1"><c r="A1" t="d"><v>2024-01-31</v></c></row>)");
EXPECT_EQ(date.type(), ValueType::date);
EXPECT_FALSE(date.has_number());
}
29 changes: 29 additions & 0 deletions test/src/internal/ooxml/ooxml_spreadsheet_write_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,35 @@ TEST(OoxmlSpreadsheetWrite, a_cleared_cell_states_nothing) {
EXPECT_EQ(sheet.cell(0, 0).value().text(), "");
}

TEST(OoxmlSpreadsheetWrite, a_boolean_lands_as_a_boolean) {
const Document document =
decode(workbook(R"(<row r="1"><c r="A1"><v>1</v></c></row>)"));
const Sheet sheet = first_sheet(document);

sheet.set_cell(0, 0, CellValue(ValueType::boolean).with_number(0));

const CellValue value = sheet.cell(0, 0).value();
EXPECT_EQ(value.type(), ValueType::boolean);
ASSERT_TRUE(value.has_number());
EXPECT_DOUBLE_EQ(value.number(), 0);
EXPECT_NE(worksheet_of(document).find(R"(<c r="A1" t="b"><v>0</v></c>)"),
std::string::npos);
}

/// No form to write these in yet, and the refusal leaves the cell as it was.
TEST(OoxmlSpreadsheetWrite, a_date_a_time_and_an_error_refuse_to_be_written) {
const Document document =
decode(workbook(R"(<row r="1"><c r="A1"><v>1</v></c></row>)"));
const Sheet sheet = first_sheet(document);

for (const ValueType type :
{ValueType::date, ValueType::time, ValueType::error}) {
EXPECT_THROW(sheet.set_cell(0, 0, CellValue(type).with_text("x")),
UnsupportedOperation);
}
EXPECT_DOUBLE_EQ(sheet.cell(0, 0).value().number(), 1);
}

TEST(OoxmlSpreadsheetWrite, a_formula_cell_refuses_to_be_written) {
const Document document = decode(
workbook(R"(<row r="1"><c r="A1"><f>SUM(B1:C1)</f><v>7</v></c></row>)"));
Expand Down
Loading