21#include <glaze/glaze.hpp>
26#include <system_error>
34namespace morph::forms::detail::engine {
37using Json = glz::generic_u64;
42[[nodiscard]]
inline std::string_view trim(std::string_view text)
noexcept {
43 constexpr std::string_view kSpace =
" \t\r\n\f\v";
44 auto const first =
text.find_first_not_of(kSpace);
45 if (first == std::string_view::npos) {
48 auto const last =
text.find_last_not_of(kSpace);
49 return text.substr(first, last - first + 1);
57[[nodiscard]]
inline std::string quote(std::string_view text) {
58 constexpr std::string_view kHex =
"0123456789abcdef";
60 out.reserve(
text.size() + 2);
62 for (
char const chr : text) {
63 auto const code =
static_cast<unsigned char>(chr);
89 out += kHex.at(code >> 4U);
90 out += kHex.at(code & 0x0FU);
105[[nodiscard]]
inline std::optional<Json> parseJson(std::string_view text) {
106 auto parsed = ui::parseJson(text);
110 return *std::move(parsed);
116[[nodiscard]]
inline std::string toJson(Json
const& value) {
return glz::write_json(value).value_or(std::string{}); }
120[[nodiscard]]
inline Json emptyObject() {
122 node.data.emplace<Json::object_t>();
130[[nodiscard]]
inline Json
const* member(Json
const& node, std::string_view key) {
131 auto const*
const object = node.get_if<Json::object_t>();
132 if (
object ==
nullptr) {
135 const auto*
const found =
object->find(key);
136 return found ==
object->end() ? nullptr : &found->second;
143[[nodiscard]]
inline std::string
const* stringAt(Json
const& node, std::string_view key) {
144 Json
const*
const found = member(node, key);
145 return found ==
nullptr ? nullptr : found->get_if<std::string>();
154[[nodiscard]]
inline std::optional<std::string> numberText(Json
const& value) {
155 if (
auto const*
const unsignedValue = value.get_if<std::uint64_t>()) {
156 return std::to_string(*unsignedValue);
158 if (
auto const*
const signedValue = value.get_if<std::int64_t>()) {
159 return std::to_string(*signedValue);
161 if (
auto const*
const doubleValue = value.get_if<
double>()) {
162 if (!std::isfinite(*doubleValue)) {
166 std::array<char, 400> buffer{};
167 auto const result = std::to_chars(std::to_address(buffer.begin()), std::to_address(buffer.end()), *doubleValue,
168 std::chars_format::fixed);
169 if (result.ec != std::errc{}) {
172 std::string
text{buffer.data(), result.ptr};
185[[nodiscard]]
inline std::string displayText(Json
const& value) {
186 if (
auto const*
const text = value.get_if<std::string>()) {
189 if (
auto number = numberText(value)) {
190 return *std::move(number);
192 if (
auto const*
const flag = value.get_if<
bool>()) {
193 return *flag ?
"true" :
"false";
195 if (value.is_null()) {
198 return toJson(value);
204[[nodiscard]]
inline bool isCanonicalDecimal(std::string_view text)
noexcept {
205 if (
text.starts_with(
'-')) {
206 text.remove_prefix(1);
208 auto const dot =
text.find(
'.');
209 auto const digits = [](std::string_view part) {
210 return !part.empty() && part.find_first_not_of(
"0123456789") == std::string_view::npos;
212 if (dot == std::string_view::npos) {
215 return digits(
text.substr(0, dot)) && digits(
text.substr(dot + 1));
221[[nodiscard]]
inline bool isCanonicalInteger(std::string_view text)
noexcept {
222 return isCanonicalDecimal(text) && !
text.contains(
'.');
228[[nodiscard]]
inline std::string stripLeadingZeros(std::string_view canonical) {
230 if (canonical.starts_with(
'-')) {
232 canonical.remove_prefix(1);
234 while (canonical.size() > 1 && canonical.front() ==
'0' && canonical.at(1) !=
'.') {
235 canonical.remove_prefix(1);
244[[nodiscard]]
constexpr std::strong_ordering reversed(std::strong_ordering order)
noexcept {
245 if (std::is_lt(order)) {
246 return std::strong_ordering::greater;
248 if (std::is_gt(order)) {
249 return std::strong_ordering::less;
251 return std::strong_ordering::equal;
258[[nodiscard]]
inline std::strong_ordering compareDecimal(std::string_view lhs, std::string_view rhs)
noexcept {
260 bool negative =
false;
261 std::string_view whole;
262 std::string_view fraction;
264 auto const split = [](std::string_view
text) {
266 parts.negative =
text.starts_with(
'-');
267 if (parts.negative) {
268 text.remove_prefix(1);
270 auto const dot =
text.find(
'.');
271 parts.whole =
text.substr(0, dot);
272 parts.fraction = dot == std::string_view::npos ? std::string_view{} :
text.substr(dot + 1);
273 while (parts.whole.size() > 1 && parts.whole.front() ==
'0') {
274 parts.whole.remove_prefix(1);
276 while (!parts.fraction.empty() && parts.fraction.back() ==
'0') {
277 parts.fraction.remove_suffix(1);
279 if ((parts.whole.empty() || parts.whole ==
"0") && parts.fraction.empty()) {
280 parts.negative =
false;
284 auto const magnitude = [](
Parts const& left,
Parts const& right) {
285 if (left.whole.size() != right.whole.size()) {
286 return left.whole.size() <=> right.whole.size();
288 if (
auto const order = left.whole <=> right.whole; std::is_neq(order)) {
291 return left.fraction <=> right.fraction;
293 Parts const left = split(lhs);
294 Parts const right = split(rhs);
295 if (left.negative != right.negative) {
296 return left.negative ? std::strong_ordering::less : std::strong_ordering::greater;
298 auto const order = magnitude(left, right);
299 if (!left.negative) {
302 return reversed(order);
309[[nodiscard]]
inline std::string multiplyDigits(std::string_view lhs, std::string_view rhs) {
310 std::vector<std::uint32_t> product(lhs.size() + rhs.size(), 0U);
311 for (std::size_t i = lhs.size(); i-- > 0;) {
312 for (std::size_t j = rhs.size(); j-- > 0;) {
313 auto const left =
static_cast<std::uint32_t
>(lhs.at(i) -
'0');
314 auto const right =
static_cast<std::uint32_t
>(rhs.at(j) -
'0');
315 std::uint32_t& low = product.at(i + j + 1);
316 std::uint32_t
const sum = (left * right) + low;
318 product.
at(i + j) +=
sum / 10U;
322 for (std::uint32_t
const digit : product) {
323 if (out.empty() && digit == 0U) {
326 out +=
static_cast<char>(
'0' + digit);
328 return out.empty() ? std::string{
"0"} : out;
338[[nodiscard]]
inline std::strong_ordering compareRationalToDecimal(math::Rational
const& value,
339 std::string_view decimal) {
340 bool const decimalNegative = decimal.starts_with(
'-');
341 std::string_view
const magnitude = decimalNegative ? decimal.substr(1) : decimal;
342 auto const dot = magnitude.find(
'.');
343 std::string digits{magnitude.substr(0, dot)};
344 std::size_t places = 0;
345 if (dot != std::string_view::npos) {
346 digits += magnitude.substr(dot + 1);
347 places = magnitude.size() - dot - 1;
349 bool const decimalZero = std::ranges::all_of(digits, [](
char chr) {
return chr ==
'0'; });
350 auto const signOf = [](
bool zero,
bool negative) {
354 return negative ? -1 : 1;
356 int const decimalSign = signOf(decimalZero, decimalNegative);
357 int const valueSign = signOf(value.numerator == 0, value.numerator < 0);
358 if (valueSign != decimalSign) {
359 return valueSign <=> decimalSign;
361 if (valueSign == 0) {
362 return std::strong_ordering::equal;
364 std::string
const scaledValue = std::to_string(math::detail::absU64(value.numerator)) + std::string(places,
'0');
365 std::string
const scaledDecimal = multiplyDigits(digits, std::to_string(value.denominator));
366 auto const order = compareDecimal(scaledValue, scaledDecimal);
367 return valueSign > 0 ? order : reversed(order);
374[[nodiscard]]
inline std::string decimalText(math::Rational
const& value, std::uint32_t places) {
375 std::uint32_t
const wanted = std::min(places, math::kMaxDecimalPlaces);
376 math::Rational
const rounded =
377 math::roundToDecimalPlaces(value, math::DecimalPlaces{wanted}, math::RoundingMode::HalfAwayFromZero);
378 std::int64_t
const scale = math::detail::powerOfTen(wanted);
379 std::string digits = multiplyDigits(std::to_string(math::detail::absU64(rounded.numerator)),
380 std::to_string(scale / rounded.denominator));
381 if (digits.size() <= wanted) {
382 digits.insert(0, wanted + 1 - digits.size(),
'0');
384 std::string out = rounded.numerator < 0 ?
"-" :
"";
385 out += std::string_view{digits}.substr(0, digits.size() - wanted);
388 out += std::string_view{digits}.substr(digits.size() - wanted);
398[[nodiscard]]
inline std::optional<int> digitsAt(std::string_view text, std::size_t offset, std::size_t count) {
399 if (offset + count >
text.size()) {
403 for (std::size_t i = offset; i < offset +
count; ++i) {
404 char const chr =
text.at(i);
405 if (chr <
'0' || chr >
'9') {
408 value = (value * 10) + (chr -
'0');
418[[nodiscard]]
inline std::optional<std::chrono::sys_seconds> parseWallClock(std::string_view text) {
419 if (
text.size() != 16 &&
text.size() != 19) {
422 if (
text.at(4) !=
'-' ||
text.at(7) !=
'-' ||
text.at(10) !=
'T' ||
text.at(13) !=
':' ||
423 (
text.size() == 19 &&
text.at(16) !=
':')) {
426 auto const year = digitsAt(text, 0, 4);
427 auto const month = digitsAt(text, 5, 2);
428 auto const day = digitsAt(text, 8, 2);
429 auto const hour = digitsAt(text, 11, 2);
430 auto const minute = digitsAt(text, 14, 2);
431 auto const second =
text.size() == 19 ? digitsAt(text, 17, 2) : std::optional<int>{0};
432 if (!year || !month || !day || !hour || !minute || !second || *hour > 23 || *minute > 59 || *second > 59) {
435 std::chrono::year_month_day
const date{std::chrono::year{*year}, std::chrono::month{
static_cast<unsigned>(*month)},
436 std::chrono::day{
static_cast<unsigned>(*day)}};
440 return std::chrono::sys_days{date} + std::chrono::hours{*hour} + std::chrono::minutes{*minute} +
441 std::chrono::seconds{*second};
450[[nodiscard]]
inline std::optional<std::chrono::sys_seconds> parseInstant(std::string_view text) {
451 std::size_t clockEnd = 19;
452 if (
text.size() < 19 ||
text.at(16) !=
':') {
455 auto local = parseWallClock(
text.substr(0, clockEnd));
459 std::string_view rest =
text.substr(clockEnd);
460 if (rest.starts_with(
'.')) {
461 rest.remove_prefix(1);
462 while (!rest.empty() && rest.front() >=
'0' && rest.front() <=
'9') {
463 rest.remove_prefix(1);
466 if (rest.empty() || rest ==
"Z") {
469 if (rest.front() !=
'+' && rest.front() !=
'-') {
472 int const sign = rest.front() ==
'-' ? -1 : 1;
473 rest.remove_prefix(1);
474 std::string compact{rest};
475 if (compact.size() == 5 && compact.at(2) ==
':') {
478 auto const hours = digitsAt(compact, 0, 2);
479 auto const minutes = digitsAt(compact, 2, 2);
480 if (compact.size() != 4 || !hours || !minutes) {
483 return *local - (sign * (std::chrono::hours{*hours} + std::chrono::minutes{*minutes}));
489[[nodiscard]]
inline std::string formatWallClock(std::chrono::sys_seconds instant) {
490 auto const days = std::chrono::floor<std::chrono::days>(instant);
491 std::chrono::year_month_day
const date{days};
492 std::chrono::hh_mm_ss
const clock{instant - days};
493 return std::format(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
static_cast<int>(date.year()),
494 static_cast<unsigned>(date.month()),
static_cast<unsigned>(date.day()), clock.hours().count(),
495 clock.minutes().count(), clock.seconds().count());
501[[nodiscard]]
inline std::string formatUtc(std::chrono::sys_seconds instant) {
return formatWallClock(instant) +
"Z"; }
506[[nodiscard]]
inline bool isIsoDate(std::string_view text) {
507 if (
text.size() != 10 ||
text.at(4) !=
'-' ||
text.at(7) !=
'-') {
510 auto const year = digitsAt(text, 0, 4);
511 auto const month = digitsAt(text, 5, 2);
512 auto const day = digitsAt(text, 8, 2);
513 if (!year || !month || !day) {
516 return std::chrono::year_month_day{std::chrono::year{*year}, std::chrono::month{
static_cast<unsigned>(*month)},
517 std::chrono::day{
static_cast<unsigned>(*day)}}
Node text(TextOptions const &options)
A text node.
AnyHandle sum(AnyHandle const &list)
sum of a list of numbers.
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
Handle< Member > at(Member Class::*member) const
A field of the value, by member pointer: a renamed field is a compile error.
@ Parts
A list of such records.
Exact rational arithmetic for morph model/action values.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...