# Program Listing for File item.hpp

↰ Return to documentation for file (`cif++/item.hpp`)

    /*-
     * SPDX-License-Identifier: BSD-2-Clause
     *
     * Copyright (c) 2022 NKI/AVL, Netherlands Cancer Institute
     *
     * Redistribution and use in source and binary forms, with or without
     * modification, are permitted provided that the following conditions are met:
     *
     * 1. Redistributions of source code must retain the above copyright notice, this
     *    list of conditions and the following disclaimer
     * 2. Redistributions in binary form must reproduce the above copyright notice,
     *    this list of conditions and the following disclaimer in the documentation
     *    and/or other materials provided with the distribution.
     *
     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
     * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
     * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
     * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
     * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
     * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
     * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
     * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     */

    #pragma once

    #include "cif++/text.hpp"

    #include <algorithm>
    #include <cassert>
    #include <compare>
    #include <cstdint>
    #include <cstring>
    #include <iostream>
    #include <optional>
    #include <stdexcept>
    #include <string>
    #include <string_view>
    #include <system_error>
    #include <type_traits>
    #include <utility>
    #include <version>

    namespace cif
    {

    class category;
    class row;

    // --------------------------------------------------------------------

    enum class item_value_type
    {
        INT,

        FLOAT,

        TEXT,

        INAPPLICABLE,

        MISSING
    };

    // --------------------------------------------------------------------

    template <typename T>
    concept IntegralType = (std::is_integral_v<std::remove_cvref_t<T>>);

    template <typename T>
    concept FloatType = std::is_floating_point_v<std::remove_cvref_t<T>>;

    template <typename T>
    concept StringType = (std::is_assignable_v<std::string, T> and not std::is_integral_v<T> and not std::is_floating_point_v<T>);

    template <typename T>
    inline constexpr bool is_optional_v = false;
    template <typename T>
    inline constexpr bool is_optional_v<std::optional<T>> = true;

    class item_value
    {
      public:
        item_value() noexcept
        {
            m_data.m_type = item_value_type::INAPPLICABLE;
        }

        item_value(std::nullptr_t)
        {
            m_data.m_type = item_value_type::MISSING;
        }

        item_value(item_value_type type) noexcept
            : m_data(type)
        {
        }

        item_value(const item_value &rhs)
        {
            m_data.m_type = rhs.m_data.m_type;
            switch (m_data.m_type)
            {
                using enum item_value_type;

                case INT:
                    m_data.m_value = rhs.m_data.m_value.m_integer;
                    break;
                case FLOAT:
                    m_data.m_len = rhs.m_data.m_len;
                    m_data.m_value = rhs.m_data.m_value.m_float;
                    break;
                case TEXT:
                    m_data.m_len = rhs.m_data.m_len;
                    m_data.m_value = rhs.m_data.sv();
                    break;
                default: break;
            }
        }

        item_value(std::string_view s)
        {
            if (s == ".")
                m_data.m_type = item_value_type::INAPPLICABLE;
            else if (s == "?")
                m_data.m_type = item_value_type::MISSING;
            else
            {
                m_data.m_type = item_value_type::TEXT;
                m_data.m_len = s.length();
                m_data.m_value = s;
            }
        }

        template <size_t N>
        item_value(const char(s)[N])
            : item_value(std::string_view{ s, N })
        {
        }

        item_value(const char *s)
            : item_value(std::string_view{ s })
        {
        }

        item_value(const std::string &s)
            : item_value(std::string_view{ s })
        {
        }

        template <IntegralType T>
        item_value(T v)
        {
            m_data.m_type = item_value_type::INT;
            m_data.m_value = static_cast<int64_t>(v);
        }

        template <FloatType T>
        item_value(T v)
        {
            m_data.m_type = item_value_type::FLOAT;
            m_data.m_value = static_cast<double>(v);
            m_data.m_len = 0;
        }

        template <FloatType T>
        item_value(T v, int precision)
        {
            m_data.m_type = item_value_type::FLOAT;
            m_data.m_value = static_cast<double>(v);
            m_data.m_len = precision;
        }

        template <typename T>
        item_value(std::optional<T> v)
        {
            if (v.has_value())
            {
                item_value iv{ *v };
                swap(*this, iv);
            }
            else
                m_data.m_type = item_value_type::MISSING;
        }

        template <FloatType T>
        item_value(std::optional<T> v, int precision)
        {
            if (v.has_value())
            {
                item_value iv{ *v };
                swap(*this, iv);
            }
            else
                m_data.m_type = item_value_type::MISSING;
            m_data.m_len = precision;
        }

        item_value(item_value &&rhs) noexcept
        {
            swap(*this, rhs);
        }

        item_value &operator=(item_value rhs) noexcept
        {
            swap(*this, rhs);
            return *this;
        }

        // --------------------------------------------------------------------

        [[nodiscard]] constexpr bool is_inapplicable() const noexcept { return m_data.m_type == item_value_type::INAPPLICABLE; }

        [[nodiscard]] constexpr bool is_missing() const noexcept { return m_data.m_type == item_value_type::MISSING; }

        [[nodiscard]] constexpr bool is_null() const noexcept { return is_inapplicable() or is_missing(); }

        [[nodiscard]] constexpr bool is_string() const noexcept { return m_data.m_type == item_value_type::TEXT; }

        [[nodiscard]] constexpr bool is_number_int() const noexcept { return m_data.m_type == item_value_type::INT; }

        [[nodiscard]] constexpr bool is_number_float() const noexcept { return m_data.m_type == item_value_type::FLOAT; }

        [[nodiscard]] constexpr bool is_number() const noexcept { return is_number_int() or is_number_float(); }

        [[nodiscard]] constexpr item_value_type type() const { return m_data.m_type; }

        [[nodiscard]] bool empty() const noexcept
        {
            switch (m_data.m_type)
            {
                using enum item_value_type;

                case INAPPLICABLE:
                case MISSING:
                    return true;

                case TEXT:
                    return m_data.sv().empty();

                default:
                    return false;
            }
        }

        // --------------------------------------------------------------------

        template <StringType T>
        [[nodiscard]] inline std::string get() const
        {
            return str();
        }

        template <IntegralType T>
        [[nodiscard]] std::remove_cvref_t<T> get() const
        {
            static_assert(not std::is_same_v<std::remove_cvref_t<T>, bool>, "bool is no longer supported");

            switch (m_data.m_type)
            {
                using enum item_value_type;

                case INT:
                    return m_data.m_value.m_integer;
                case FLOAT:
                    return m_data.m_value.m_float;
                case TEXT:
                {
                    auto sv = m_data.sv();
                    int64_t v;
                    auto &&[ptr, ec] = from_chars(sv.data(), sv.data() + sv.length(), v);
                    if (ec != std::errc{})
                        throw std::system_error(std::make_error_code(ec));
                    if (ptr != sv.data() + sv.length())
                        throw std::invalid_argument("String value does not contain only an integer");

                    return v;
                }
                default:
                    return not empty();
            }
        }

        template <FloatType T>
        [[nodiscard]] std::remove_cvref_t<T> get() const
        {
            switch (m_data.m_type)
            {
                using enum item_value_type;

                case INT:
                    return m_data.m_value.m_integer;
                case FLOAT:
                    return m_data.m_value.m_float;
                case TEXT:
                {
                    auto sv = m_data.sv();
                    double v;
                    auto &&[ptr, ec] = from_chars(sv.data(), sv.data() + sv.length(), v);
                    if (ec != std::errc{})
                        throw std::system_error(std::make_error_code(ec));
                    if (ptr != sv.data() + sv.length())
                        throw std::invalid_argument("String value does not contain only a floating point number");
                    return v;
                }
                default:
                    return not empty();
            }
        }

        template <typename T>
            requires is_optional_v<T>
        [[nodiscard]] auto get() const
        {
            switch (m_data.m_type)
            {
                using enum item_value_type;

                case INAPPLICABLE:
                case MISSING:
                    return T{};

                default:
                {
                    auto v = get<typename T::value_type>();
                    return T{ v };
                }
            }
        }

        [[nodiscard]] std::string str() const;

        [[nodiscard]] const std::string_view sv() const
        {
            assert(m_data.m_type == cif::item_value_type::TEXT);
            return m_data.sv();
        }

        // --------------------------------------------------------------------

        friend void swap(item_value &a, item_value &b) noexcept
        {
            std::swap(a.m_data.m_type, b.m_data.m_type);
            std::swap(a.m_data.m_len, b.m_data.m_len);
            std::swap(a.m_data.m_value, b.m_data.m_value);
        }

        // --------------------------------------------------------------------

        auto operator<=>(const item_value &rhs) const noexcept
        {
            std::partial_ordering result = std::partial_ordering::unordered;
            if (m_data.m_type == rhs.m_data.m_type)
            {
                switch (m_data.m_type)
                {
                    using enum item_value_type;

                    case INT: result = m_data.m_value.m_integer <=> rhs.m_data.m_value.m_integer;
                    case FLOAT: result = m_data.m_value.m_float <=> rhs.m_data.m_value.m_float;
                    case TEXT: result = m_data.sv() <=> rhs.m_data.sv();
                    default: result = std::partial_ordering::equivalent;
                }
            }
            else
                result = m_data.m_type <=> rhs.m_data.m_type;
            return result;
        }

        bool operator==(const item_value &rhs) const
        {
            if (m_data.m_type == rhs.m_data.m_type)
            {
                switch (m_data.m_type)
                {
                    using enum item_value_type;

                    case INT: return m_data.m_value.m_integer == rhs.m_data.m_value.m_integer;
                    case FLOAT: return m_data.m_value.m_float == rhs.m_data.m_value.m_float;
                    case TEXT: return m_data.sv() == rhs.m_data.sv();
                    case INAPPLICABLE:
                    case MISSING: return true;
                }
            }

            return false;
        }

        [[nodiscard]] int compare(const item_value &b, bool ignore_case = false) const noexcept;

        friend std::ostream &operator<<(std::ostream &os, const item_value &v);

        void cast_to_int();

        void cast_to_float();

        void cast_to_string();

      private:
        union value
        {
            int64_t m_integer{};
            double m_float;
            char m_local_str[8];
            char *m_str;

            value() = default;

            value(int64_t v)
                : m_integer(v)
            {
            }

            value(double v)
                : m_float(v)
            {
            }

            value(std::string_view s)
            {
                if (s.length() >= sizeof(m_local_str))
                {
                    m_str = new char[s.length() + 1];
                    std::copy(s.data(), s.data() + s.length(), m_str);
                    m_str[s.length()] = 0;
                }
                else
                    std::memcpy(m_local_str, s.data(), s.length() + 1);
            }

            value(item_value_type t)
            {
                m_integer = 0;
            }

            void destroy(item_value_type t, size_t len)
            {
                if (t == item_value_type::TEXT and len >= sizeof(m_local_str))
                    delete[] m_str;
            }
        };

        struct data
        {
            item_value_type m_type = item_value_type::MISSING;
            uint32_t m_len{};
            value m_value{};

            data(item_value_type t)
                : m_type(t)
                , m_value(t)
            {
            }

            data() noexcept = default;
            data(data &&rhs) noexcept
            {
                std::swap(m_type, rhs.m_type);
                std::swap(m_len, rhs.m_len);
                std::swap(m_value, rhs.m_value);
            }

            data(const data &) noexcept = delete;
            data &operator=(data &&) noexcept = delete;
            data &operator=(const data &) noexcept = delete;

            ~data()
            {
                m_value.destroy(m_type, m_len);
            }

            [[nodiscard]] std::string_view sv() const noexcept
            {
                assert(m_type == item_value_type::TEXT);
                return m_type == item_value_type::TEXT ? std::string_view(m_len >= sizeof(m_value.m_local_str) ? m_value.m_str : m_value.m_local_str, m_len) : std::string_view{};
            }

            [[nodiscard]] const char *c_str() const noexcept
            {
                assert(m_type == item_value_type::TEXT);
                return m_type == item_value_type::TEXT ? (m_len >= sizeof(m_value.m_local_str) ? m_value.m_str : m_value.m_local_str) : nullptr;
            }
        } m_data{};

    };

    static_assert(sizeof(item_value) == 16, "item_value should be 16 bytes");

    class item
    {
      public:
        item() = default;

        item(std::string name)
            : m_name(std::move(name))
            , m_value(item_value_type::MISSING)
        {
        }

        item(std::string name, item_value value)
            : m_name(std::move(name))
            , m_value(std::move(value))
        {
        }

        item(const item &rhs) = default;

        item(item &&rhs)
        {
            swap(*this, rhs);
        }

        item &operator=(item rhs) noexcept
        {
            swap(*this, rhs);
            return *this;
        }

        friend void swap(item &a, item &b) noexcept
        {
            std::swap(a.m_name, b.m_name);
            std::swap(a.m_value, b.m_value);
        }

        [[nodiscard]] const std::string &name() const { return m_name; }
        [[nodiscard]] const item_value &value() const & { return m_value; }
        item_value &value() & { return m_value; }

        void value(item_value v) { m_value = std::move(v); }

        [[nodiscard]] bool empty() const { return m_value.empty(); }

        [[nodiscard]] bool is_null() const { return m_value.is_null(); }

        [[nodiscard]] bool is_unknown() const { return m_value.is_missing(); }

        // /// \brief the length of the value string
        // std::size_t length() const { return m_value.length(); }

        template <std::size_t N>
        decltype(auto) get() const
        {
            if constexpr (N == 0)
                return name();
            else if constexpr (N == 1)
                return value();
        }

        // auto operator<=>(const item &rhs) const = default;

      private:
        std::string m_name;
        item_value m_value;
    };

    // --------------------------------------------------------------------

    struct item_handle
    {
      public:
        item_handle() = delete;

        item_handle &operator=(item_value value)
        {
            set(std::move(value), true);
            return *this;
        }

        [[nodiscard]] item_value &value();

        [[nodiscard]] const item_value &value() const;

        [[nodiscard]] bool is_inapplicable() const noexcept
        {
            return not empty() and value().type() == item_value_type::INAPPLICABLE;
        }

        [[nodiscard]] bool is_missing() const noexcept
        {
            return empty() or value().type() == item_value_type::MISSING;
        }

        [[nodiscard]] bool is_null() const noexcept
        {
            return empty() or is_inapplicable() or is_missing();
        }

        [[nodiscard]] bool is_string() const noexcept
        {
            return not empty() and value().type() == item_value_type::TEXT;
        }

        [[nodiscard]] bool is_number_int() const noexcept
        {
            return not empty() and value().type() == item_value_type::INT;
        }

        [[nodiscard]] bool is_number_float() const noexcept
        {
            return not empty() and value().type() == item_value_type::FLOAT;
        }

        [[nodiscard]] bool is_number() const noexcept
        {
            return not empty() and (is_number_int() or is_number_float());
        }

        [[nodiscard]] auto type() const
        {
            return empty() ? item_value_type::MISSING : value().type();
        }

        template <typename T>
        [[nodiscard]] auto get() const
        {
            if (empty())
                return T{};
            else
                return value().template get<T>();
        }

        template <typename T>
        [[deprecated("Use get<T> instead")]] [[nodiscard]] auto as() const
        {
            if (empty())
                return T{};
            else
                return value().template get<T>();
        }

        [[nodiscard]] auto str() const
        {
            return value().str();
        }

        [[nodiscard]] auto sv() const
        {
            return value().sv();
        }

        friend void swap(item_handle a, item_handle b) noexcept;

        template <typename T>
        [[nodiscard]] auto value_or(const T &dv) const
        {
            return empty() ? dv : this->get<T>();
        }

        [[nodiscard]] int compare(const item_value &value, bool icase = true) const noexcept
        {
            return this->value().compare(value, icase);
        }

         [[nodiscard]] int compare(const item_handle &value, bool icase = true) const noexcept
        {
            if (empty() and value.empty())
                return 0;
            else if (empty())
                return -1;
            else if (value.empty())
                return 1;
            else
                return compare(value.value(), icase);
        }

        [[nodiscard]] bool operator==(const item_value &value) const noexcept
        {
            // TODO: icase or not icase?
            return this->value().compare(value) == 0;
        }

        // We may not have C++20 yet...

        template <typename T>
        [[nodiscard]] bool operator!=(const T &value) const noexcept
        {
            return not operator==(value);
        }

        [[nodiscard]] bool empty() const;

        [[nodiscard]] std::string_view text_() const;

        item_handle(category &cat, row &row, uint16_t item_ix)
            : m_category(cat)
            , m_row(row)
            , m_item_ix(item_ix)
        {
        }

        item_handle(const category &cat, const row &r, uint16_t item_ix)
            : m_category(const_cast<category &>(cat))
            , m_row(const_cast<row &>(r))
            , m_item_ix(item_ix)
            , m_is_const(true)
        {
        }

        item_handle(const item_handle &) = delete;
        item_handle &operator=(const item_handle &) = delete;

        friend std::ostream &operator<<(std::ostream &os, const item_handle &h)
        {
            if (h.empty())
                os << "NULL";
            else
                os << h.value();
            return os;
        }

      private:
        category &m_category;
        row &m_row;
        uint16_t m_item_ix;
        bool m_is_const = false;

        friend class parser;

        void set(item_value value, bool updateLinked);
    };

    } // namespace cif

    namespace std
    {

    template <>
    struct tuple_size<::cif::item>
        : public std::integral_constant<std::size_t, 2>
    {
    };

    template <>
    struct tuple_element<0, ::cif::item>
    {
        using type = decltype(std::declval<::cif::item>().name());
    };

    template <>
    struct tuple_element<1, ::cif::item>
    {
        using type = decltype(std::declval<::cif::item>().value());
    };

    } // namespace std