diff --git a/tagvalue.cpp b/tagvalue.cpp index 8c2c774..d103ca5 100644 --- a/tagvalue.cpp +++ b/tagvalue.cpp @@ -266,8 +266,6 @@ bool TagValue::compareTo(const TagValue &other, TagValueComparisionFlags options } case TagDataType::PositionInSet: return toPositionInSet() == other.toPositionInSet(); - case TagDataType::Integer: - return toInteger() == other.toInteger(); case TagDataType::StandardGenreIndex: return toStandardGenreIndex() == other.toStandardGenreIndex(); case TagDataType::TimeSpan: @@ -298,7 +296,11 @@ bool TagValue::compareTo(const TagValue &other, TagValueComparisionFlags options } // handle types where an implicit conversion to the specific type can be done - if (m_type == TagDataType::Popularity || other.m_type == TagDataType::Popularity) { + if (m_type == TagDataType::Integer || other.m_type == TagDataType::Integer) { + return toInteger() == other.toInteger(); + } else if (m_type == TagDataType::UnsignedInteger || other.m_type == TagDataType::UnsignedInteger) { + return toUnsignedInteger() == other.toUnsignedInteger(); + } else if (m_type == TagDataType::Popularity || other.m_type == TagDataType::Popularity) { if (options & TagValueComparisionFlags::CaseInsensitive) { const auto lhs = toPopularity(), rhs = other.toPopularity(); return lhs.rating == rhs.rating && lhs.playCounter == rhs.playCounter && compareData(lhs.user, rhs.user, true); @@ -391,6 +393,51 @@ std::int32_t TagValue::toInteger() const throw ConversionException("Can not convert assigned data to integer because the data size is not appropriate."); case TagDataType::Popularity: return static_cast(toPopularity().rating); + case TagDataType::UnsignedInteger: { + const auto unsignedInteger = toUnsignedInteger(); + if (unsignedInteger > std::numeric_limits::max()) { + throw ConversionException(argsToString("Unsigned integer too big for conversion to integer.")); + } + return static_cast(unsignedInteger); + } + default: + throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to integer.")); + } +} + +std::uint64_t TagValue::toUnsignedInteger() const +{ + if (isEmpty()) { + return 0; + } + switch (m_type) { + case TagDataType::Text: + switch (m_encoding) { + case TagTextEncoding::Utf16LittleEndian: + case TagTextEncoding::Utf16BigEndian: { + auto u16str = u16string(reinterpret_cast(m_ptr.get()), m_size / 2); + ensureHostByteOrder(u16str, m_encoding); + return stringToNumber(u16str); + } + default: + return bufferToNumber(m_ptr.get(), m_size); + } + case TagDataType::PositionInSet: + case TagDataType::Integer: + case TagDataType::StandardGenreIndex: { + const auto integer = toInteger(); + if (integer < 0) { + throw ConversionException(argsToString("Can not convert negative value to unsigned integer.")); + } + return static_cast(integer); + } + case TagDataType::Popularity: + return static_cast(toPopularity().rating); + case TagDataType::UnsignedInteger: + if (m_size == sizeof(std::uint64_t)) { + return *reinterpret_cast(m_ptr.get()); + } + throw ConversionException("Can not convert assigned data to unsigned integer because the data size is not appropriate."); default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to integer.")); } @@ -423,10 +470,19 @@ int TagValue::toStandardGenreIndex() const } case TagDataType::StandardGenreIndex: case TagDataType::Integer: - if (m_size != sizeof(std::int32_t)) { + case TagDataType::UnsignedInteger: + if (m_size == sizeof(std::int32_t)) { + index = static_cast(*reinterpret_cast(m_ptr.get())); + } else if (m_size == sizeof(std::uint64_t)) { + const auto unsignedInt = *reinterpret_cast(m_ptr.get()); + if (unsignedInt <= std::numeric_limits::max()) { + index = static_cast(unsignedInt); + } else { + index = Id3Genres::genreCount(); + } + } else { throw ConversionException("The assigned index/integer is of unappropriate size."); } - index = static_cast(*reinterpret_cast(m_ptr.get())); break; default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to genre index.")); @@ -470,6 +526,17 @@ PositionInSet TagValue::toPositionInSet() const default: throw ConversionException("The size of the assigned data is not appropriate."); } + case TagDataType::UnsignedInteger: + switch (m_size) { + case sizeof(std::uint64_t): { + const auto track = *(reinterpret_cast(m_ptr.get())); + if (track < std::numeric_limits::max()) { + return PositionInSet(static_cast(track)); + } + } + default:; + } + throw ConversionException("The size of the assigned data is not appropriate."); default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to position in set.")); } @@ -498,6 +565,17 @@ TimeSpan TagValue::toTimeSpan() const default: throw ConversionException("The size of the assigned integer is not appropriate for conversion to time span."); } + case TagDataType::UnsignedInteger: + switch (m_size) { + case sizeof(std::uint64_t): { + const auto ticks = *(reinterpret_cast(m_ptr.get())); + if (ticks < std::numeric_limits::max()) { + return TimeSpan(static_cast(ticks)); + } + } + default:; + } + throw ConversionException("The size of the assigned data is not appropriate."); default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to time span.")); } @@ -524,9 +602,10 @@ DateTime TagValue::toDateTime() const } case TagDataType::Integer: case TagDataType::DateTime: + case TagDataType::UnsignedInteger: if (m_size == sizeof(std::int32_t)) { return DateTime(*(reinterpret_cast(m_ptr.get()))); - } else if (m_size == sizeof(std::int64_t)) { + } else if (m_size == sizeof(std::uint64_t)) { return DateTime(*(reinterpret_cast(m_ptr.get()))); } else { throw ConversionException("The size of the assigned integer is not appropriate for conversion to date time."); @@ -568,6 +647,9 @@ Popularity TagValue::toPopularity() const } break; } + case TagDataType::UnsignedInteger: + popularity.rating = static_cast(toUnsignedInteger()); + break; default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to date time.")); } @@ -751,6 +833,9 @@ void TagValue::toString(string &result, TagTextEncoding encoding) const case TagDataType::Popularity: result = toPopularity().toString(); break; + case TagDataType::UnsignedInteger: + result = numberToString(toUnsignedInteger()); + break; default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string.")); } @@ -838,6 +923,9 @@ void TagValue::toWString(std::u16string &result, TagTextEncoding encoding) const case TagDataType::Popularity: regularStrRes = toPopularity().toString(); break; + case TagDataType::UnsignedInteger: + regularStrRes = numberToString(toUnsignedInteger()); + break; default: throw ConversionException(argsToString("Can not convert ", tagDataTypeString(m_type), " to string.")); } @@ -918,6 +1006,19 @@ void TagValue::assignInteger(int value) m_encoding = TagTextEncoding::Latin1; } +/*! + * \brief Assigns the given unsigned integer \a value. + * \param value Specifies the unsigned integer to be assigned. + */ +void TagValue::assignUnsignedInteger(std::uint64_t value) +{ + m_size = sizeof(value); + m_ptr = make_unique(m_size); + std::copy(reinterpret_cast(&value), reinterpret_cast(&value) + m_size, m_ptr.get()); + m_type = TagDataType::UnsignedInteger; + m_encoding = TagTextEncoding::Latin1; +} + /*! * \brief Assigns a copy of the given \a data. * \param data Specifies the data to be assigned. diff --git a/tagvalue.h b/tagvalue.h index f169748..f0fc0fc 100644 --- a/tagvalue.h +++ b/tagvalue.h @@ -102,6 +102,7 @@ enum class TagDataType : unsigned int { Binary, /**< unspecified binary data */ Undefined, /**< undefined/invalid data type */ Popularity, /**< rating with user info and play counter (as in ID3v2's "Popularimeter") */ + UnsignedInteger, /**< unsigned integer */ }; /*! @@ -126,6 +127,7 @@ public: explicit TagValue( std::string_view text, TagTextEncoding textEncoding = TagTextEncoding::Latin1, TagTextEncoding convertTo = TagTextEncoding::Unspecified); explicit TagValue(int value); + explicit TagValue(std::uint64_t value); explicit TagValue( const char *data, std::size_t length, TagDataType type = TagDataType::Undefined, TagTextEncoding encoding = TagTextEncoding::Latin1); explicit TagValue(std::unique_ptr &&data, std::size_t length, TagDataType type = TagDataType::Binary, @@ -158,6 +160,7 @@ public: std::u16string toWString(TagTextEncoding encoding = TagTextEncoding::Unspecified) const; void toWString(std::u16string &result, TagTextEncoding encoding = TagTextEncoding::Unspecified) const; std::int32_t toInteger() const; + std::uint64_t toUnsignedInteger() const; int toStandardGenreIndex() const; PositionInSet toPositionInSet() const; CppUtilities::TimeSpan toTimeSpan() const; @@ -194,6 +197,7 @@ public: void assignText( std::string_view text, TagTextEncoding textEncoding = TagTextEncoding::Latin1, TagTextEncoding convertTo = TagTextEncoding::Unspecified); void assignInteger(int value); + void assignUnsignedInteger(std::uint64_t value); void assignStandardGenreIndex(int index); void assignData(const char *data, std::size_t length, TagDataType type = TagDataType::Binary, TagTextEncoding encoding = TagTextEncoding::Latin1); void assignData(std::unique_ptr &&data, std::size_t length, TagDataType type = TagDataType::Binary, @@ -318,6 +322,14 @@ inline TagValue::TagValue(int value) { } +/*! + * \brief Constructs a new TagValue holding the given unsigned integer \a value. + */ +inline TagParser::TagValue::TagValue(std::uint64_t value) + : TagValue(reinterpret_cast(&value), sizeof(value), TagDataType::UnsignedInteger) +{ +} + /*! * \brief Constructs a new TagValue with a copy of the given \a data. * diff --git a/tests/tagvalue.cpp b/tests/tagvalue.cpp index 4c74d3b..d640ceb 100644 --- a/tests/tagvalue.cpp +++ b/tests/tagvalue.cpp @@ -24,6 +24,7 @@ class TagValueTests : public TestFixture { CPPUNIT_TEST(testBasics); CPPUNIT_TEST(testBinary); CPPUNIT_TEST(testInteger); + CPPUNIT_TEST(testUnsignedInteger); CPPUNIT_TEST(testPositionInSet); CPPUNIT_TEST(testTimeSpan); CPPUNIT_TEST(testDateTime); @@ -39,6 +40,7 @@ public: void testBasics(); void testBinary(); void testInteger(); + void testUnsignedInteger(); void testPositionInSet(); void testTimeSpan(); void testDateTime(); @@ -77,10 +79,11 @@ void TagValueTests::testBinary() void TagValueTests::testInteger() { // positive number - TagValue integer(42); + auto integer = TagValue(42); CPPUNIT_ASSERT(!integer.isEmpty()); CPPUNIT_ASSERT_EQUAL(TagDataType::Integer, integer.type()); CPPUNIT_ASSERT_EQUAL(static_cast(42), integer.toInteger()); + CPPUNIT_ASSERT_EQUAL(static_cast(42), integer.toUnsignedInteger()); CPPUNIT_ASSERT_EQUAL("42"s, integer.toString()); integer.assignInteger(2); CPPUNIT_ASSERT_EQUAL("Country"s, string(Id3Genres::stringFromIndex(integer.toStandardGenreIndex()))); @@ -107,16 +110,41 @@ void TagValueTests::testInteger() CPPUNIT_ASSERT_MESSAGE("cleared vale considered empty", integer.isEmpty()); CPPUNIT_ASSERT_EQUAL_MESSAGE("only date (but not type) cleared"s, TagDataType::Integer, integer.type()); CPPUNIT_ASSERT_EQUAL(static_cast(0), integer.toInteger()); + CPPUNIT_ASSERT_EQUAL(static_cast(0), integer.toUnsignedInteger()); CPPUNIT_ASSERT_EQUAL(string(), integer.toString()); CPPUNIT_ASSERT_EQUAL(DateTime(), integer.toDateTime()); CPPUNIT_ASSERT_EQUAL(TimeSpan(), integer.toTimeSpan()); } +void TagValueTests::testUnsignedInteger() +{ + auto unsignedInteger = TagValue(static_cast(42ul)); + CPPUNIT_ASSERT(!unsignedInteger.isEmpty()); + CPPUNIT_ASSERT_EQUAL(TagDataType::UnsignedInteger, unsignedInteger.type()); + CPPUNIT_ASSERT_EQUAL(static_cast(42), unsignedInteger.toInteger()); + CPPUNIT_ASSERT_EQUAL(static_cast(42), unsignedInteger.toUnsignedInteger()); + CPPUNIT_ASSERT_EQUAL("42"s, unsignedInteger.toString()); + unsignedInteger.assignUnsignedInteger(2); + CPPUNIT_ASSERT_EQUAL("Country"s, string(Id3Genres::stringFromIndex(unsignedInteger.toStandardGenreIndex()))); + unsignedInteger.assignInteger(Id3Genres::emptyGenreIndex()); + CPPUNIT_ASSERT_EQUAL(Id3Genres::emptyGenreIndex(), unsignedInteger.toStandardGenreIndex()); + unsignedInteger.clearData(); + CPPUNIT_ASSERT_EQUAL(Id3Genres::emptyGenreIndex(), unsignedInteger.toStandardGenreIndex()); + + // zero + unsignedInteger.assignInteger(0); + CPPUNIT_ASSERT_MESSAGE("explicitly assigned zero not considered empty", !unsignedInteger.isEmpty()); + CPPUNIT_ASSERT_EQUAL("0"s, unsignedInteger.toString()); + CPPUNIT_ASSERT_EQUAL(DateTime(), unsignedInteger.toDateTime()); + CPPUNIT_ASSERT_EQUAL(TimeSpan(), unsignedInteger.toTimeSpan()); +} + void TagValueTests::testPositionInSet() { const TagValue test(PositionInSet(4, 23)); CPPUNIT_ASSERT_EQUAL(PositionInSet(4, 23), test.toPositionInSet()); - CPPUNIT_ASSERT_EQUAL(test.toInteger(), 4); + CPPUNIT_ASSERT_EQUAL(4, test.toInteger()); + CPPUNIT_ASSERT_EQUAL(static_cast(4), test.toUnsignedInteger()); CPPUNIT_ASSERT_EQUAL("4/23"s, test.toString()); CPPUNIT_ASSERT_THROW(test.toStandardGenreIndex(), ConversionException); CPPUNIT_ASSERT_THROW(test.toDateTime(), ConversionException); @@ -170,6 +198,7 @@ void TagValueTests::testString() CPPUNIT_ASSERT_EQUAL("\x31\0\x35\0"s, TagValue(15).toString(TagTextEncoding::Utf16LittleEndian)); CPPUNIT_ASSERT_EQUAL("\0\x31\0\x35"s, TagValue(15).toString(TagTextEncoding::Utf16BigEndian)); CPPUNIT_ASSERT_EQUAL(15, TagValue("\0\x31\0\x35"s, TagTextEncoding::Utf16BigEndian).toInteger()); + CPPUNIT_ASSERT_EQUAL(static_cast(15), TagValue("\0\x31\0\x35"s, TagTextEncoding::Utf16BigEndian).toUnsignedInteger()); CPPUNIT_ASSERT_EQUAL_MESSAGE( "original encoding preserved", "15ä"s, TagValue("15ä", 4, TagTextEncoding::Utf8).toString(TagTextEncoding::Unspecified)); CPPUNIT_ASSERT_EQUAL_MESSAGE("original encoding preserved", "\0\x31\0\x35"s,