31#include <morph/core/callback_scope.hpp>
39#include <unordered_map>
44namespace morph::table {
63using RowId = std::variant<std::int64_t, std::string>;
83using Cell = std::variant<std::monostate, std::int64_t, math::Rational, double, std::string, bool, QuantityCell>;
132[[nodiscard]]
inline std::expected<math::Rational, TableError>
parseDecimal(std::string_view text) {
133 auto const fail = [&] {
137 std::size_t cursor = 0;
138 bool negative =
false;
139 if (cursor < text.size() && (text[cursor] ==
'-' || text[cursor] ==
'+')) {
140 negative = text[cursor] ==
'-';
143 std::uint64_t magnitude = 0;
144 std::uint32_t fractionDigits = 0;
145 std::size_t digits = 0;
146 bool inFraction =
false;
149 auto const bound = negative ?
static_cast<std::uint64_t
>(std::numeric_limits<std::int64_t>::max()) + 1U
150 :
static_cast<std::uint64_t
>(std::numeric_limits<std::int64_t>::max());
151 for (; cursor < text.size(); ++cursor) {
152 char const character = text[cursor];
153 if (character ==
'.' && !inFraction) {
157 if (character <
'0' || character >
'9') {
160 auto const digit =
static_cast<std::uint64_t
>(character -
'0');
161 if (magnitude > (bound - digit) / 10U) {
164 magnitude = (magnitude * 10U) + digit;
173 std::int64_t
const numerator =
174 negative ?
static_cast<std::int64_t
>(0U - magnitude) :
static_cast<std::int64_t
>(magnitude);
175 if (fractionDigits == 0) {
180 if (numerator == std::numeric_limits<std::int64_t>::min()) {
183 std::int64_t denominator = 1;
184 for (std::uint32_t i = 0; i < fractionDigits; ++i) {
198 []<
typename T>(T
const& value) -> std::string {
199 if constexpr (std::is_same_v<T, std::monostate>) {
201 }
else if constexpr (std::is_same_v<T, std::string>) {
203 }
else if constexpr (std::is_same_v<T, bool>) {
204 return value ?
"true" :
"false";
205 }
else if constexpr (std::is_same_v<T, QuantityCell>) {
206 return units::detail::formatRationalDecimal(value.amount);
207 }
else if constexpr (std::is_same_v<T, math::Rational>) {
208 return units::detail::formatRationalDecimal(value);
210 return std::format(
"{}", value);
232 [[nodiscard]]
virtual std::string
sortKey(std::string_view text)
const = 0;
238 [[nodiscard]]
virtual std::string
fold(std::string_view text)
const = 0;
242 [[nodiscard]]
virtual std::shared_ptr<TextCollator const>
cloneForTask()
const {
return nullptr; }
255 [[nodiscard]] std::string
sortKey(std::string_view text)
const override {
return fold(text); }
261 [[nodiscard]] std::string
fold(std::string_view text)
const override {
263 out.reserve(text.size());
264 for (std::size_t i = 0; i < text.size(); ++i) {
265 auto const byte =
static_cast<unsigned char>(text[i]);
266 if (
byte >=
'A' &&
byte <=
'Z') {
267 out.push_back(
static_cast<char>(
byte + (
'a' -
'A')));
272 if (
byte == 0xC3 && i + 1 < text.size()) {
273 auto const next =
static_cast<unsigned char>(text[i + 1]);
274 if (next >= 0x80 && next <= 0x9E && next != 0x97) {
275 out.push_back(text[i]);
276 out.push_back(
static_cast<char>(next + 0x20));
281 out.push_back(text[i]);
301 [[nodiscard]]
virtual std::optional<std::int64_t>
parseDate(std::string_view text,
302 std::string_view format)
const = 0;
308 [[nodiscard]]
virtual std::optional<std::int64_t>
parseDateTime(std::string_view text,
309 std::string_view format)
const = 0;
313 [[nodiscard]]
virtual std::shared_ptr<DateParser const>
cloneForTask()
const {
return nullptr; }
329 [[nodiscard]] std::optional<std::int64_t>
parseDate(std::string_view text,
330 std::string_view format)
const override {
331 static_cast<void>(format);
332 if (text.size() != 10 || text[4] !=
'-' || text[7] !=
'-') {
335 auto const number = [&](std::size_t offset, std::size_t count) -> std::optional<int> {
337 for (std::size_t i = offset; i < offset + count; ++i) {
338 if (text[i] <
'0' || text[i] >
'9') {
341 value = (value * 10) + (text[i] -
'0');
345 auto const year = number(0, 4);
346 auto const month = number(5, 2);
347 auto const day = number(8, 2);
348 if (!year || !month || !day) {
351 std::chrono::year_month_day
const date{std::chrono::year{*year},
352 std::chrono::month{
static_cast<unsigned>(*month)},
353 std::chrono::day{
static_cast<unsigned>(*day)}};
357 return std::chrono::sys_days{date}.time_since_epoch().count();
364 [[nodiscard]] std::optional<std::int64_t>
parseDateTime(std::string_view text,
365 std::string_view format)
const override {
366 if (
auto const days =
parseDate(text, format)) {
367 return *days * std::int64_t{86400};
373 return std::chrono::floor<std::chrono::seconds>(instant->value).time_since_epoch().count();
386 virtual void progress(std::size_t done, std::size_t total) = 0;
400 std::shared_ptr<DateParser const>
dates;
409 static auto const fallback = std::make_shared<CodePointCollator const>();
416 static auto const fallback = std::make_shared<IsoDateParser const>();
426 if (
auto clone = out.
collator->cloneForTask()) {
430 if (
auto clone = out.
dates->cloneForTask()) {
431 out.
dates = std::move(clone);
448 virtual void cells(std::size_t firstRow, std::span<Cell const>
cells) = 0;
459 RowSnapshot() =
default;
460 virtual ~RowSnapshot() =
default;
464 [[nodiscard]]
virtual std::size_t
rowCount()
const = 0;
469 [[nodiscard]]
virtual RowId rowId(std::size_t row)
const = 0;
475 [[nodiscard]]
virtual Cell cell(std::size_t row, std::size_t column)
const = 0;
487 for (std::size_t row = first; row < last; ++row) {
488 Cell const value =
cell(row, column);
489 sink.
cells(row, std::span<Cell const>{&value, 1});
529 [[nodiscard]]
static std::shared_ptr<TableSnapshot const>
fromRows(std::size_t
columnCount, std::vector<RowId> ids,
530 std::vector<std::vector<Cell>> rows) {
531 auto out = std::make_shared<TableSnapshot>(
columnCount);
532 out->assign(std::move(ids), std::move(rows), 0);
538 [[nodiscard]] std::size_t
rowCount()
const override {
return _rows; }
542 [[nodiscard]] std::size_t
columnCount() const noexcept {
return _columnCount; }
555 [[nodiscard]]
Cell cell(std::size_t row, std::size_t column)
const override {
565 while (first < last) {
566 auto const& chunk = *_chunks[first /
kChunkRows];
568 auto const count = std::min(last - first, chunk.ids.size() - offset);
569 sink.
cells(first, std::span<Cell const>{chunk.columns[column]}.subspan(offset, count));
579 [[nodiscard]] std::shared_ptr<TableSnapshot const>
withRow(std::size_t row, std::vector<Cell> cells)
const {
580 auto out = std::make_shared<TableSnapshot>(*
this);
581 auto chunk = std::make_shared<Chunk>(*_chunks[row /
kChunkRows]);
582 cells.resize(_columnCount);
583 for (std::size_t column = 0; column < _columnCount; ++column) {
584 chunk->columns[column][row %
kChunkRows] = std::move(cells[column]);
586 out->_chunks[row /
kChunkRows] = std::move(chunk);
596 std::vector<Cell> cells)
const {
597 auto [ids, rows] = tail(row);
598 ids.insert(ids.begin(), std::move(key));
599 rows.insert(rows.begin(), std::move(cells));
600 auto out = std::make_shared<TableSnapshot>(*
this);
601 out->assign(std::move(ids), std::move(rows), row);
608 [[nodiscard]] std::shared_ptr<TableSnapshot const>
withRemoved(std::size_t row)
const {
609 auto [ids, rows] = tail(row);
610 ids.erase(ids.begin());
611 rows.erase(rows.begin());
612 auto out = std::make_shared<TableSnapshot>(*
this);
613 out->assign(std::move(ids), std::move(rows), row);
627 std::vector<RowId> ids;
628 std::vector<std::vector<Cell>> columns;
632 [[nodiscard]] std::pair<std::vector<RowId>, std::vector<std::vector<Cell>>> tail(std::size_t first)
const {
633 std::vector<RowId> ids;
634 std::vector<std::vector<Cell>> rows;
635 ids.reserve(_rows - first);
636 rows.reserve(_rows - first);
637 for (std::size_t row = first; row < _rows; ++row) {
638 ids.push_back(
rowId(row));
639 std::vector<Cell> cells;
640 cells.reserve(_columnCount);
641 for (std::size_t column = 0; column < _columnCount; ++column) {
642 cells.push_back(
cell(row, column));
644 rows.push_back(std::move(cells));
646 return {std::move(ids), std::move(rows)};
651 void assign(std::vector<RowId> ids, std::vector<std::vector<Cell>> rows, std::size_t first) {
653 std::vector<RowId> headIds;
654 std::vector<std::vector<Cell>> headRows;
655 if (keepChunks < _chunks.size()) {
656 headIds.reserve(first - (keepChunks *
kChunkRows));
657 headRows.reserve(first - (keepChunks *
kChunkRows));
659 headIds.push_back(
rowId(row));
660 std::vector<Cell>
cells;
661 cells.reserve(_columnCount);
662 for (std::size_t column = 0;
column < _columnCount; ++
column) {
665 headRows.push_back(std::move(cells));
668 _chunks.resize(std::min(keepChunks, _chunks.size()));
669 headIds.insert(headIds.end(), std::make_move_iterator(ids.begin()), std::make_move_iterator(ids.end()));
670 headRows.insert(headRows.end(), std::make_move_iterator(rows.begin()), std::make_move_iterator(rows.end()));
671 _rows = (_chunks.size() *
kChunkRows) + headIds.size();
672 for (std::size_t at = 0; at < headIds.size(); at +=
kChunkRows) {
673 auto chunk = std::make_shared<Chunk>();
674 auto const end = std::min(at +
kChunkRows, headIds.size());
675 chunk->ids.assign(std::make_move_iterator(headIds.begin() +
static_cast<std::ptrdiff_t
>(at)),
676 std::make_move_iterator(headIds.begin() +
static_cast<std::ptrdiff_t
>(end)));
677 chunk->columns.resize(_columnCount);
678 for (
auto& column : chunk->columns) {
681 for (std::size_t row = at;
row < end; ++
row) {
683 cells.resize(_columnCount);
684 for (std::size_t column = 0;
column < _columnCount; ++
column) {
685 chunk->columns[
column].push_back(std::move(cells[column]));
688 _chunks.push_back(std::move(chunk));
692 std::size_t _columnCount = 0;
693 std::size_t _rows = 0;
694 std::vector<std::shared_ptr<Chunk const>> _chunks;
738 [[nodiscard]]
virtual std::span<ColumnInfo const>
columns()
const = 0;
742 [[nodiscard]]
virtual std::shared_ptr<RowSnapshot const>
snapshot()
const = 0;
776 auto const alive = _alive.token();
777 auto const listeners = _listeners;
778 for (
auto*
const listener : listeners) {
779 if (alive.expired()) {
782 if (std::ranges::find(_listeners, listener) != _listeners.end()) {
783 listener->rowsChanged(change);
789 std::vector<ChangeListener*> _listeners;
816 : _columns{std::move(
columns)}, _snapshot{std::make_shared<
TableSnapshot const>(_columns.size())} {}
820 [[nodiscard]] std::span<ColumnInfo const>
columns()
const override {
return _columns; }
824 [[nodiscard]] std::shared_ptr<RowSnapshot const>
snapshot()
const override {
return _snapshot; }
828 [[nodiscard]] std::shared_ptr<TableSnapshot const>
const&
tableSnapshot() const noexcept {
return _snapshot; }
842 void setRows(std::vector<RowId> ids, std::vector<std::vector<Cell>> rows,
843 std::optional<std::vector<ColumnInfo>>
columns = std::nullopt) {
845 _columns = std::move(*
columns);
855 void updateRow(std::size_t row, std::vector<Cell> cells) {
856 _snapshot = _snapshot->withRow(row, std::move(cells));
865 auto const found = _index.find(key);
866 if (found == _index.end()) {
869 updateRow(found->second, std::move(cells));
878 _snapshot = _snapshot->withInserted(row, std::move(key), std::move(cells));
886 _snapshot = _snapshot->withRemoved(row);
894 [[nodiscard]] std::optional<std::size_t>
indexOf(
RowId const& key)
const {
895 auto const found = _index.find(key);
896 return found == _index.end() ? std::nullopt : std::optional{found->second};
902 for (std::size_t row = 0; row < _snapshot->rowCount(); ++row) {
903 _index.emplace(_snapshot->rowId(row), row);
907 std::vector<ColumnInfo> _columns;
908 std::shared_ptr<TableSnapshot const> _snapshot;
909 std::unordered_map<RowId, std::size_t> _index;
910 detail::Listeners _listeners;
919struct glz::meta<morph::table::ColumnKind> {
922 static constexpr auto value =
923 glz::enumerate(
"integer", Integer,
"decimal", Decimal,
"quantity", Quantity,
"number", Number,
"text", Text,
924 "date", Date,
"dateTime", DateTime,
"bool", Bool,
"key", Key,
"custom", Custom);
929struct glz::meta<morph::table::TableErrorCode> {
932 static constexpr auto value =
933 glz::enumerate(
"unknownColumn", UnknownColumn,
"unsupportedOperator", UnsupportedOperator,
"invalidValue",
934 InvalidValue,
"invalidSpec", InvalidSpec,
"limitExceeded", LimitExceeded,
"columnNotReadable",
935 ColumnNotReadable,
"unavailable", Unavailable,
"serviceFailed", ServiceFailed);
Node column(Nodes const &children, ContainerOptions const &options={})
A column node.
Handle< T > row()
The innermost row, {"row": ""}.
std::vector< TableColumn > cells(std::vector< Node > const &cells)
Columns from cells alone, without ids or labels.
Owned lifetime-and-stop gate for asynchronous callbacks — a data member, not a base class.
Receives a DataSource's change notifications, on the owner thread.
virtual void rowsChanged(RowChange const &change)=0
Called after the source changed; its snapshot() already shows the change.
The default collator: code-point order, with ASCII and Latin-1 letters case-folded....
std::string fold(std::string_view text) const override
ASCII A-Z and Latin-1 À-Þ (except ×) lowered; every other byte copied as it is.
std::string sortKey(std::string_view text) const override
The case-folded text; its bytes order as its code points.
Receives a run of one column's cells.
virtual void cells(std::size_t firstRow, std::span< Cell const > cells)=0
Receives consecutive cells of one column.
The rows of a table, as the engine reads them.
virtual std::span< ColumnInfo const > columns() const =0
The table's columns.
virtual std::shared_ptr< RowSnapshot const > snapshot() const =0
The current rows as an immutable snapshot. Taking one is cheap.
virtual void unsubscribe(ChangeListener &listener)=0
Removes listener.
virtual void subscribe(ChangeListener &listener)=0
Registers listener for change notifications.
Parses text to dates and date-times for Date and DateTime columns that hold text, and for filter valu...
virtual std::optional< std::int64_t > parseDateTime(std::string_view text, std::string_view format) const =0
Parses a date-time.
virtual std::shared_ptr< DateParser const > cloneForTask() const
An instance for one task to use alone.
virtual std::optional< std::int64_t > parseDate(std::string_view text, std::string_view format) const =0
Parses a date.
The default date parser: ISO 8601 only, whatever the format.
std::optional< std::int64_t > parseDateTime(std::string_view text, std::string_view format) const override
Parses an ISO 8601 date-time, or a bare date as midnight UTC.
std::optional< std::int64_t > parseDate(std::string_view text, std::string_view format) const override
Parses YYYY-MM-DD.
Receives progress of a long sort or filter, on the owner thread.
virtual void progress(std::size_t done, std::size_t total)=0
Reports progress through the current job.
Replaces a row by key; what a committed cell edit patches.
virtual bool patchRow(RowId const &key, std::vector< Cell > cells)=0
Replaces the cells of the row keyed id.
RowSnapshot & operator=(RowSnapshot &&)=default
Move-assigns the base of a snapshot.
virtual Cell cell(std::size_t row, std::size_t column) const =0
One cell.
virtual RowId rowId(std::size_t row) const =0
A row's key.
RowSnapshot & operator=(RowSnapshot const &)=default
Copy-assigns the base of a snapshot.
RowSnapshot(RowSnapshot const &)=default
Copies the base of a snapshot: a derived snapshot makes a changed copy of itself this way.
RowSnapshot(RowSnapshot &&)=default
Moves the base of a snapshot.
virtual void readColumn(std::size_t column, ColumnSink &sink, std::size_t first, std::size_t last) const
Hands rows [first, last) of column to sink.
virtual std::size_t rowCount() const =0
The number of rows.
A RowSnapshot that stores rows in shared, immutable chunks.
std::shared_ptr< TableSnapshot const > withRow(std::size_t row, std::vector< Cell > cells) const
A copy of this snapshot with one row's cells replaced. Only that row's chunk is copied.
static constexpr std::size_t kChunkRows
Rows per chunk: small enough that copying one is cheap, large enough that a column read hands long ru...
void readColumn(std::size_t column, ColumnSink &sink, std::size_t first, std::size_t last) const override
Hands rows [first, last) of column to sink, one run per chunk.
Cell cell(std::size_t row, std::size_t column) const override
One cell.
std::size_t rowCount() const override
The number of rows.
std::shared_ptr< TableSnapshot const > withRemoved(std::size_t row) const
A copy with row row removed. Chunks before it are shared.
bool sharesChunk(TableSnapshot const &other, std::size_t row) const noexcept
Whether this snapshot and other share the chunk holding row.
std::size_t columnCount() const noexcept
The number of columns.
std::shared_ptr< TableSnapshot const > withInserted(std::size_t row, RowId key, std::vector< Cell > cells) const
A copy with a row inserted before row. Chunks before it are shared.
static std::shared_ptr< TableSnapshot const > fromRows(std::size_t columnCount, std::vector< RowId > ids, std::vector< std::vector< Cell > > rows)
Builds a snapshot from rows.
RowId rowId(std::size_t row) const override
A row's key.
TableSnapshot(std::size_t columnCount)
Constructs an empty snapshot.
Orders and matches text. The renderer injects the toolkit's collator for the user's locale; CodePoint...
virtual std::string sortKey(std::string_view text) const =0
A key whose byte order is the collation order of text.
virtual std::string fold(std::string_view text) const =0
text folded to primary strength: two texts a user would call equal ignoring case and accents fold to ...
virtual std::shared_ptr< TextCollator const > cloneForTask() const
An instance for one task to use alone.
std::optional< std::size_t > indexOf(RowId const &key) const
The index of the row keyed id.
bool patchRow(RowId const &key, std::vector< Cell > cells) override
Replaces the cells of the row keyed id, then notifies Updated.
void updateRow(std::size_t row, std::vector< Cell > cells)
Replaces one row's cells, then notifies Updated.
std::shared_ptr< RowSnapshot const > snapshot() const override
The current rows; a pointer copy.
void setRows(std::vector< RowId > ids, std::vector< std::vector< Cell > > rows, std::optional< std::vector< ColumnInfo > > columns=std::nullopt)
Replaces every row (and optionally the columns), then notifies Reset.
std::span< ColumnInfo const > columns() const override
The table's columns.
void unsubscribe(ChangeListener &listener) override
Removes listener; safe from inside a notification.
void removeRow(std::size_t row)
Removes row row, then notifies Removed.
std::shared_ptr< TableSnapshot const > const & tableSnapshot() const noexcept
The current rows, typed.
void subscribe(ChangeListener &listener) override
Registers listener.
void insertRow(std::size_t row, RowId key, std::vector< Cell > cells)
Inserts a row before row, then notifies Inserted.
VectorSource(std::vector< ColumnInfo > columns)
Constructs an empty source with columns.
A source's listeners, notified so that a listener may unsubscribe itself or another,...
void remove(ChangeListener &listener)
Removes listener.
void add(ChangeListener &listener)
Registers listener.
void notify(RowChange const &change)
Calls every listener registered now that is still registered when its turn comes.
std::variant< std::monostate, std::int64_t, math::Rational, double, std::string, bool, QuantityCell > Cell
One cell as a data source hands it out.
ChangeKind
How rows changed.
@ Inserted
rows are the new rows' indices in the new snapshot.
@ Reset
Everything may have changed, the columns included.
@ Removed
rows are the removed rows' indices in the old snapshot.
@ Updated
rows changed their cells; row indices are unchanged.
std::function< std::strong_ordering(Cell const &, Cell const &)> CellComparator
Orders two cells of a Custom column. Called from the worker, so it must be safe to call concurrently.
ColumnKind
What a column holds, which decides how its keys are built and compared.
@ Custom
Ordered by a named comparator from Services::comparators.
@ Text
Text, ordered and matched through the TextCollator.
TableErrorCode
What went wrong in a fallible table call.
@ InvalidValue
A filter value that does not parse for its column's kind.
@ LimitExceeded
A TableQuery over one of the server's bounds.
@ Unavailable
An operation the table's mode does not offer.
@ UnsupportedOperator
A filter operator the column's kind does not define.
@ ServiceFailed
An injected service (collator, date parser, comparator) threw.
@ InvalidSpec
A filter entry with no operator or more than one.
@ ColumnNotReadable
A sort or filter on a column the principal may not read.
@ UnknownColumn
A sort or filter names a column the table does not have.
std::expected< math::Rational, TableError > parseDecimal(std::string_view text)
Parses exact decimal text ("-12.50") into a Rational.
std::string displayText(Cell const &cell)
The text a cell shows when it has to be ordered or matched as text.
std::variant< std::int64_t, std::string > RowId
A row's identity: the table's key, stable across reloads.
UTC timestamps for morph actions.
Unit-tagged, optionally-empty exact values with a derivation trail.
Exact rational arithmetic for morph model/action values.
constexpr std::uint32_t kMaxDecimalPlaces
Largest decimal precision the type supports: 10^18 is the greatest power of ten that fits in std::int...
Strong type for a decimal-precision count.
Strong type for a Rational denominator.
Strong type for a Rational numerator.
Exact rational number with a runtime decimal-precision tag.
A column's identity and kind.
std::string comparator
For a Custom column, the name of its comparator in Services::comparators.
std::string format
For a Date or DateTime column holding text, the format handed to the DateParser.
bool operator==(ColumnInfo const &other) const =default
Member-wise equality.
std::string id
The column's id, unique within the table (the row member's name).
ColumnKind kind
How the column's keys are built and compared.
A quantity in its own unit, with the factor that converts it to the column's canonical unit.
math::Rational amount
The amount in the cell's own unit.
math::Rational toCanonical
Multiplier from the cell's unit to the column's canonical unit.
ChangeKind kind
What happened.
std::vector< std::size_t > rows
The rows concerned; empty for Reset.
The services an engine sorts and filters with. A null member is replaced by its default.
std::shared_ptr< DateParser const > dates
Date parsing; null means IsoDateParser.
Services forTask() const
The services one task uses: collator and parser cloned when they are not safe to share,...
std::shared_ptr< DateParser const > datesOrDefault() const
The date parser to use.
std::shared_ptr< TextCollator const > collatorOrDefault() const
The collator to use.
std::shared_ptr< ProgressSink > progress
Optional progress receiver.
std::shared_ptr< TextCollator const > collator
Text order and matching; null means CodePointCollator.
std::map< std::string, CellComparator, std::less<> > comparators
Comparators for Custom columns, by the name ColumnInfo::comparator gives.
The error every fallible table call returns.
TableErrorCode code
What went wrong.
std::string message
A human-readable description.
std::string column
The column concerned, or empty when the error is not about one column.
static std::optional< DateTime > fromIso8601(std::string_view text) noexcept
Strictly parses ISO-8601 UTC: YYYY-MM-DDTHH:MM:SS, optionally followed by .m/.mm/....