/* * libfud * Copyright 2024 Dominick Allen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #ifndef FUD_FORMAT_HPP #define FUD_FORMAT_HPP // #include "fud_assert.hpp" #include "fud_option.hpp" #include "fud_result.hpp" #include "fud_status.hpp" #include "fud_string.hpp" #include "fud_string_convert.hpp" #include "fud_string_view.hpp" #include "fud_utf8.hpp" #include #include #include namespace fud { struct FormatString { template consteval FormatString(const char (&input)[N]) : m_size{N - 1}, m_data{input} { static_assert(N > 0); } StringView view() { return StringView{m_size, reinterpret_cast(m_data)}; } size_t m_size; const char* m_data; }; struct FormatAlign { enum class Value : utf8 { Left = '<', Right = '>', Center = '^', Default = std::numeric_limits::max() }; constexpr static FormatAlign makeDefault() noexcept { return {Value::Default}; } constexpr static Option from(utf8 letter) { FormatAlign formatAlign; switch (letter) { case '<': formatAlign.value = Value::Left; break; case '>': formatAlign.value = Value::Right; break; case '^': formatAlign.value = Value::Center; break; default: return NullOpt; } return formatAlign; } Value operator()() const { return value; } Value value; }; struct FormatFill { FormatAlign align; utf8 fill; constexpr static FormatFill make() noexcept { return FormatFill{FormatAlign::makeDefault(), ' '}; } constexpr static Result, FudStatus> make(StringView formatView, size_t& length) noexcept { using RetType = Result, FudStatus>; if (formatView.length() < 1) { return RetType::okay(NullOpt); } const auto* data = formatView.data(); auto align1 = FormatAlign::from(data[0]); decltype(align1) align2 = NullOpt; if (formatView.length() > 1) { align2 = FormatAlign::from(data[1]); } if (align2.hasValue()) { length = 2; auto fill = data[0]; if (not Ascii::valid(fill)) { return FudStatus::Utf8Invalid; } if (fill == '{' || fill == '}') { return FudStatus::FormatInvalid; } return RetType::okay(FormatFill{std::move(align2).value(), data[0]}); } if (align1.hasValue()) { length = 1; return RetType::okay(FormatFill{std::move(align1).value(), ' '}); } return RetType::okay(NullOpt); } }; enum class FormatSign : uint8_t { Plus = '+', Minus = '-', Space = ' ', Default = std::numeric_limits::max() }; enum class FormatType : utf8 { Unspecified = '\0', String = 's', Escaped = '?', BinaryLower = 'b', BinaryUpper = 'B', Character = 'c', Decimal = 'd', Octal = 'o', HexLower = 'x', HexUpper = 'X', FloatHexLower = 'a', FloatHexUpper = 'A', ScientificLower = 'e', ScientificUpper = 'E', FixedLower = 'f', FixedUpper = 'F', GeneralLower = 'g', GeneralUpper = 'G', }; struct FormatSpec; using FormatSpecResult = Result; struct FormatSpec { static constexpr uint32_t widthUnspecified = std::numeric_limits::max(); static constexpr uint32_t precisionUnspecified = std::numeric_limits::max(); static constexpr uint16_t positionUnspecified = std::numeric_limits::max(); static constexpr utf8 openBracket = '{'; static constexpr utf8 closeBracket = '}'; static constexpr utf8 formatTypeUnspecified = std::numeric_limits::max(); static constexpr utf8 localeChar = 'L'; uint32_t width{widthUnspecified}; uint32_t precision{precisionUnspecified}; uint16_t position{positionUnspecified}; FormatFill fill{FormatFill::make()}; FormatSign formatSign{FormatSign::Default}; FormatType formatType{}; bool takesWidth : 1 = false; bool takesPrecision : 1 = false; bool alternateForm : 1 = false; bool leadingZero : 1 = false; bool hasLocale : 1 = false; [[nodiscard]] constexpr bool takesPosition() const { return position == positionUnspecified; } static Result parse(StringView formatView, size_t& specLength); }; namespace impl { struct FormatHandle { void* object; auto operator<=>(const FormatHandle& rhs) const noexcept = default; }; } // namespace impl using FormatArgument = std::variant< std::monostate, bool, utf8, int32_t, uint32_t, int64_t, uint64_t, float, double, long double, const utf8*, StringView, const void*, impl::FormatHandle>; template struct FormatArguments { Array arguments; constexpr size_t size() const { return Size; } template static auto makeFormatArguments(Args&&... args) -> FormatArguments { return FormatArguments{Array{{FormatArgument{args}...}}}; } }; static_assert(sizeof(FormatArguments<1>) > 0); enum class FormatCharMode { Unchecked, AsciiOnly, ValidUtf8 }; struct FormatResult { size_t bytesWritten{0}; FudStatus status{FudStatus::Failure}; [[nodiscard]] constexpr bool isOkay() { return status == FudStatus::Success; } }; /* TODO : require concept of a sink that takes pushBack() -> FudStatus */ /* TODO : sink concept also requires append() -> FudStatus */ template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatString fmt); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatString fmt, Args&&... args); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, bool arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, utf8 arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, int32_t arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, uint32_t arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, int64_t arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, uint64_t arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, float arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, double arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, long double arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, const utf8* arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, StringView arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, const void* arg); template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatSpec& formatSpec, impl::FormatHandle arg); namespace impl { Result validateSpecHelper(FormatString fmt); struct FormatInfo { uint16_t numArgs{0}; uint16_t maxArg{0}; bool positional{false}; }; Result validateSpecHelperV2(FormatString fmt); template FormatResult vFormat(Sink& sink, FormatCharMode formatMode, FormatString fmt, FormatArguments& args); } // namespace impl template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatString fmt, Args&&... args) { static_cast(sink); if (formatMode != FormatCharMode::Unchecked) { return {0, FudStatus::NotImplemented}; } constexpr size_t numArgs = sizeof...(args); auto formatArguments{FormatArguments::makeFormatArguments(std::forward(args)...)}; static_cast(formatArguments); auto numSpecsResult = impl::validateSpecHelper(fmt); if (numSpecsResult.isError()) { return {0, numSpecsResult.takeError()}; } if (numSpecsResult.getOkay() < numArgs) { return {0, FudStatus::FormatInvalid}; } return impl::vFormat(sink, formatMode, fmt, formatArguments); } template FormatResult format(Sink& sink, FormatCharMode formatMode, FormatString fmt) { static_cast(sink); static_cast(fmt); if (formatMode != FormatCharMode::Unchecked) { return {0, FudStatus::NotImplemented}; } return {0, FudStatus::NotImplemented}; } namespace impl { bool nestingHelper(bool& sawBracket, int& nesting, int increment) { auto inSpec = nesting > 0; if (not inSpec && not sawBracket) { sawBracket = true; return false; } if (not inSpec && sawBracket) { sawBracket = false; return false; } nesting += increment; return true; } Result validateSpecHelper(FormatString fmt) { auto scanView{fmt.view()}; const auto length = scanView.length(); const auto* data = scanView.data(); int nesting = 0; size_t index = 0; size_t numGroups = 0; bool sawOpenBracket = false; bool sawCloseBracket = false; while (index < length) { auto letter = data[index]; if (letter == FormatSpec::openBracket) { auto incremented = nestingHelper(sawOpenBracket, nesting, 1); if (incremented) { numGroups++; } } else if (letter == FormatSpec::closeBracket) { nestingHelper(sawCloseBracket, nesting, -1); if (sawOpenBracket) { sawOpenBracket = false; sawCloseBracket = false; numGroups++; } } else { if (sawOpenBracket) { nesting++; numGroups++; } sawOpenBracket = false; sawCloseBracket = false; } if (nesting < 0 || nesting > 2) { return FudStatus::FormatInvalid; } ++index; } if (nesting != 0 || sawOpenBracket || sawCloseBracket) { return FudStatus::FormatInvalid; } return numGroups; } // NOLINTBEGIN(readability-function-cognitive-complexity) Result validateSpecHelperV2(FormatString fmt) { auto scanView{fmt.view()}; const auto length = scanView.length(); const auto* data = scanView.data(); int nesting = 0; size_t index = 0; FormatInfo formatInfo{}; bool sawOpenBracket = false; bool sawCloseBracket = false; bool enteredGroup = false; bool firstGroup = true; while (index < length) { auto inSpec = nesting > 0; auto letter = data[index]; if (letter == FormatSpec::openBracket) { if (not inSpec) { sawOpenBracket = not sawOpenBracket; } else { nesting++; formatInfo.numArgs++; enteredGroup = true; } } else if (letter == FormatSpec::closeBracket) { if (not inSpec) { sawCloseBracket = not sawCloseBracket; } else { nesting--; } if (sawOpenBracket) { sawOpenBracket = false; sawCloseBracket = false; enteredGroup = false; formatInfo.numArgs++; } } else if (enteredGroup) { nesting++; formatInfo.numArgs++; enteredGroup = false; sawOpenBracket = false; sawCloseBracket = false; if (classify::isDigit(letter)) { if (length <= index) { return FudStatus::FormatInvalid; } StringView specView{length - index, data + index}; auto positionResult = fromString(specView, Radix::Decimal); if (positionResult.isError()) { return positionResult.takeError(); } auto positionValue = positionResult.takeOkay(); if (not firstGroup && not formatInfo.positional) { return FudStatus::FormatInvalid; } formatInfo.maxArg = std::max(formatInfo.maxArg, positionValue.value); formatInfo.numArgs++; firstGroup = false; } } if (nesting < 0 || nesting > 2) { return FudStatus::FormatInvalid; } ++index; } if (nesting != 0 || sawOpenBracket || sawCloseBracket) { return FudStatus::FormatInvalid; } return formatInfo; } // NOLINTEND(readability-function-cognitive-complexity) size_t findSpec(StringView scanView) { size_t index = 0; bool foundBracket = false; while (index < scanView.length()) { auto letter = scanView[index]; if (letter == FormatSpec::openBracket && not foundBracket) { foundBracket = true; } else if (letter == FormatSpec::openBracket && foundBracket) { foundBracket = false; } else if (foundBracket) { index--; break; } index++; } return index; } template Result vFormatPrepareArgs( StringView& scanView, FormatArguments& args, size_t& argIndex, bool& firstSpec, bool& takesPosition) { size_t specLength{0}; auto formatSpecResult{FormatSpec::parse(scanView, specLength)}; if (formatSpecResult.isError()) { return formatSpecResult; } fudAssert(specLength <= scanView.length()); auto formatSpec{formatSpecResult.takeOkay()}; bool specTakesPosition = formatSpec.position != FormatSpec::positionUnspecified; if (firstSpec) { takesPosition = specTakesPosition; firstSpec = false; } else if (takesPosition != specTakesPosition) { return FudStatus::FormatInvalid; } else if (takesPosition && formatSpec.position >= Size) { return FudStatus::FormatInvalid; } if (formatSpec.takesWidth) { if (takesPosition) { auto widthPosition = formatSpec.width; if (widthPosition >= args.size()) { return FudStatus::FormatInvalid; } } } // auto formatResult{format(sink, scanView.advanceUnsafe(specLength); return formatSpec; } template FormatResult vFormat(Sink& sink, FormatCharMode formatMode, FormatString fmt, FormatArguments& args) { FormatResult result{0, FudStatus::Failure}; auto scanView = fmt.view(); bool firstSpec = false; bool takesPosition = false; size_t argIndex = 0; while (scanView.length() > 0) { auto specIndex = findSpec(scanView); fudAssert(specIndex <= scanView.length()); StringView run{specIndex, scanView.data()}; auto status = sink.append(run); if (status != FudStatus::Success) { result.status = status; return result; } result.bytesWritten += specIndex; scanView.advanceUnsafe(specIndex); if (specIndex == scanView.length()) { return result; } auto specResult{vFormatPrepareArgs(scanView, args, argIndex, firstSpec, takesPosition)}; } static_cast(formatMode); static_cast(args); return result; } } // namespace impl namespace fudTest { inline void test() { String sink{}; static_cast(format(sink, FormatCharMode::Unchecked, "HELL YEAH")); static_cast(format(sink, FormatCharMode::Unchecked, "HELL YEAH", 1, true, "ye")); } } // namespace fudTest } // namespace fud #endif