diff --git a/include/objc-helpers/BoxUtil.h b/include/objc-helpers/BoxUtil.h index 4e74726..1499db5 100644 --- a/include/objc-helpers/BoxUtil.h +++ b/include/objc-helpers/BoxUtil.h @@ -120,7 +120,7 @@ namespace BoxMakerDetail __attribute__((visibility("hidden"))) { template class __attribute__((visibility("hidden"))) BoxMaker { private: - static auto detectBoxedType() { + static consteval auto detectBoxedType() { if constexpr (std::totally_ordered) { if constexpr (std::is_copy_constructible_v) { return (NSObject *)nullptr; diff --git a/include/objc-helpers/NSStringUtil.h b/include/objc-helpers/NSStringUtil.h index f2e30f5..6296c57 100644 --- a/include/objc-helpers/NSStringUtil.h +++ b/include/objc-helpers/NSStringUtil.h @@ -368,17 +368,33 @@ class NSStringCharAccess CFIndex _size = 0; }; +/** + Converts CFStringRef to `std::basic_string` + + @tparam Char character type of the resulatant std::basic_string + @param str string to convert. If nullptr an empty string is returned + @param start start index. If less than 0 assumed to be 0. If greater than length of the string an empty string is returned. + @param length number of source characters to convert. If less than 0 or start + length exceeds the length of the string assumed "to the end of string". + + Convertions to char16_t are exact and never fail. Conversions to other character types are transcodings and can fail if source string contains invalid UTF-16 + sequences. In such cases an empty string is returned. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template auto makeStdString(CFStringRef __nullable str, CFIndex start = 0, CFIndex length = -1) { using RetType = std::basic_string; + if (!str) return RetType(); + auto strLength = CFStringGetLength(str); + if (start >= strLength) + return RetType(); if (start < 0) start = 0; - if (length < 0) { - length = CFStringGetLength(str); - length -= std::min(length, start); - } + if (length < 0) + length = strLength - start; + else + length = std::min(length, strLength - start); + if constexpr (std::is_same_v) { RetType ret(size_t(length), '\0'); CFStringGetCharacters(str, {start, length}, (UniChar *)ret.data()); @@ -397,6 +413,17 @@ auto makeStdString(CFStringRef __nullable str, CFIndex start = 0, CFIndex length #ifdef __OBJC__ +/** + Converts NSString to `std::basic_string` + + @tparam Char character type of the resulatant std::basic_string + @param str string to convert. If nil an empty string is returned + @param location start index. If greater than length of the string an empty string is returned. + @param length number of source characters to convert. If start + length exceeds the length of the string assumed "to the end of string". + + Convertions to char16_t are exact and never fail. Conversions to other character types are transcodings and can fail if source string contains invalid UTF-16 + sequences. In such cases an empty string is returned. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template inline auto makeStdString(NSString * __nullable str, NSUInteger location = 0, NSUInteger length = NSUInteger(-1)) { return makeStdString((__bridge CFStringRef)str, CFIndex(location), CFIndex(length)); @@ -404,18 +431,50 @@ inline auto makeStdString(NSString * __nullable str, NSUInteger location = 0, NS #endif +/** + Converts a range denoted by a pair of NSStringCharAccess::const_iterator to `std::basic_string` + + @tparam Char character type of the resulatant std::basic_string + + If iterators do not form a valid substring of some NSString the behavior is undefined. If first and alst are both uninitialized iterators the result is an empty string. + + Convertions to char16_t are exact and never fail. Conversions to other character types are transcodings and can fail if source string contains invalid UTF-16 + sequences. In such cases an empty string is returned. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template inline auto makeStdString(NSStringCharAccess::const_iterator first, NSStringCharAccess::const_iterator last) { return makeStdString(first.getCFString(), first.index(), last.index() - first.index()); } - +/** + Converts NSStringCharAccess or a view derived from it to `std::basic_string` + + @tparam Char character type of the resulatant std::basic_string + + A dervied view must produce the original view iterators. Thus, for example, `std::views::take(N) would work while + `std::views::reverse` will not (and won't compile) since the latter's iterators are not the same as NSStringCharAccess ones. + + If iterators do not form a valid substring of some NSString the behavior is undefined. If first and alst are both uninitialized iterators the result is an empty string. + + Convertions to char16_t are exact and never fail. Conversions to other character types are transcodings and can fail if source string contains invalid UTF-16 + sequences. In such cases an empty string is returned. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template requires(std::is_same_v>, NSStringCharAccess::const_iterator>) inline auto makeStdString(Range && range) { return makeStdString(std::ranges::begin(range), std::ranges::end(range)); } +/** + Converts any contiguous range of characters to CFString + + @returns nullptr on failure + + The type of range's characters can be any of: char, char16_t, char32_t, char8_t, wchar_t. + + Convertions from char16_t are exact and can only fail if out of memory. Conversions from other character types are + transcodings and can fail if source string contains invalid UTF sequences. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template requires(CharTypeConvertibleWithNSString>) inline auto makeCFString(const CharRange & range) -> CFStringRef __nullable { @@ -430,6 +489,16 @@ inline auto makeCFString(const CharRange & range) -> CFStringRef __nullable { } } +/** + Converts a null terminated character string to CFString + + @returns nullptr on failure + + The type of range's characters can be any of: char, char16_t, char32_t, char8_t, wchar_t. + + Convertions from char16_t are exact and can only fail if out of memory. Conversions from other character types are + transcodings and can fail if source string contains invalid UTF sequences. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template inline auto makeCFString(const Char * __nullable str) -> CFStringRef __nullable { if (!str) @@ -437,6 +506,16 @@ inline auto makeCFString(const Char * __nullable str) -> CFStringRef __nullable return makeCFString(std::basic_string_view(str)); } +/** + Converts an initializer list of characters to CFString + + @returns nullptr on failure + + The type of range's characters can be any of: char, char16_t, char32_t, char8_t, wchar_t. + + Convertions from char16_t are exact and can only fail if out of memory. Conversions from other character types are + transcodings and can fail if source string contains invalid UTF sequences. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template inline auto makeCFString(const std::initializer_list & str) { return makeCFString(std::basic_string_view(str.begin(), str.size())); @@ -445,17 +524,47 @@ inline auto makeCFString(const std::initializer_list & str) { #ifdef __OBJC__ +/** + Converts any contiguous range of characters to NSString + + @returns nil on failure + + The type of range's characters can be any of: char, char16_t, char32_t, char8_t, wchar_t. + + Convertions from char16_t are exact and can only fail if out of memory. Conversions from other character types are + transcodings and can fail if source string contains invalid UTF sequences. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template requires(CharTypeConvertibleWithNSString>) inline auto makeNSString(const CharRange & range) { return (__bridge_transfer NSString *)makeCFString(range); } +/** + Converts a null terminated character string to NSString + + @returns nil on failure + + The type of range's characters can be any of: char, char16_t, char32_t, char8_t, wchar_t. + + Convertions from char16_t are exact and can only fail if out of memory. Conversions from other character types are + transcodings and can fail if source string contains invalid UTF sequences. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template inline auto makeNSString(const Char * __nullable str) { return (__bridge_transfer NSString *)makeCFString(str); } +/** + Converts an initializer list of characters to NSString + + @returns nil on failure + + The type of range's characters can be any of: char, char16_t, char32_t, char8_t, wchar_t. + + Convertions from char16_t are exact and can only fail if out of memory. Conversions from other character types are + transcodings and can fail if source string contains invalid UTF sequences. Encodings of char and wchar_t are UTF-8 and UTF-32 respectively. + */ template inline auto makeNSString(const std::initializer_list & str) { return (__bridge_transfer NSString *)makeCFString(str); diff --git a/test/BoxUtilTests.mm b/test/BoxUtilTests.mm index aa8c308..9179542 100644 --- a/test/BoxUtilTests.mm +++ b/test/BoxUtilTests.mm @@ -33,6 +33,24 @@ CHECK([box(6) compare:box(5)] == NSOrderedDescending); CHECK([box(6) compare:box(6)] == NSOrderedSame); + auto b = box(27); + CHECK([b compare:b] == NSOrderedSame); + + @try { + NSObject * n; + [box(5) compare:n]; + FAIL("able to compare with nil"); + } @catch (NSException * exc) { + CHECK([exc.name isEqualToString:NSInvalidArgumentException]); + } + + @try { + [box(5) compare:box(5l)]; + FAIL("able to compare with different class"); + } @catch (NSException * exc) { + CHECK([exc.name isEqualToString:NSInvalidArgumentException]); + } + @try { [[maybe_unused]] auto obj1 = (NSObject *)[[obj.class alloc] init]; FAIL("able to call init"); diff --git a/test/NSStringUtilTests.mm b/test/NSStringUtilTests.mm index a8ba553..c90281f 100644 --- a/test/NSStringUtilTests.mm +++ b/test/NSStringUtilTests.mm @@ -98,6 +98,7 @@ auto * str = @"abc"; CHECK(r::equal(NSStringCharAccess(str), u"abc"sv)); CHECK(r::equal(NSStringCharAccess(str) | v::reverse, u"cba"sv)); + CHECK(NSStringCharAccess(str).getString() == str); } { auto * str = @"яж"; @@ -110,11 +111,15 @@ CHECK(r::equal(NSStringCharAccess(str) | v::reverse, u"57"sv)); } + CHECK(NSStringCharAccess(nullptr).getString() == nullptr); + CHECK(NSStringCharAccess::iterator().getString() == nullptr); + auto access = NSStringCharAccess(@"abcd"); auto it1 = access.begin() + 1; auto it2 = it1 + 2; auto substr = [access.getString() substringWithRange:{NSUInteger(it1.index()), NSUInteger(it2.index() - it1.index())}]; CHECK(NSStringEqual()(substr, @"bc")); + CHECK(it1.getString() == it2.getString()); CHECK(it1 < it2); CHECK(it1 <= it2); CHECK(it2 > it1); @@ -130,6 +135,7 @@ CHECK(it2 == access.begin()); CHECK(it1 - it2 == 1); CHECK(1 + it1 == it1 + 1); + CHECK(it2[1] == *it1); it1 -= 1; CHECK(it1 == it2); it1 += 1; @@ -149,6 +155,7 @@ CHECK(NSStringCharAccess(@"").empty()); CHECK(CFStringCompare(NSStringCharAccess(@"abcd").getCFString(), CFSTR("abcd"), 0) == 0); + CHECK(NSStringCharAccess(nullptr).getCFString() == nullptr); } TEST_CASE("makeNSString") { @@ -282,6 +289,7 @@ auto malformed = makeNSString(u"\xD800"); { + CHECK(makeStdString((NSString *)nullptr) == u""); CHECK(makeStdString(@"abc") == u"abc"); CHECK(makeStdString(@"abc", 1) == u"bc"); CHECK(makeStdString(@"abc", 1, 1) == u"b"); @@ -290,6 +298,7 @@ CHECK(makeStdString(malformed) == u"\xD800"); } { + CHECK(makeStdString((NSString *)nullptr) == ""); CHECK(makeStdString(@"abc") == "abc"); CHECK(makeStdString(@"abc", 1) == "bc"); CHECK(makeStdString(@"abc", 1, 1) == "b"); @@ -298,6 +307,7 @@ CHECK(makeStdString(malformed) == ""); } { + CHECK(makeStdString((NSString *)nullptr) == u8""); CHECK(makeStdString(@"abc") == u8"abc"); CHECK(makeStdString(@"abc", 1) == u8"bc"); CHECK(makeStdString(@"abc", 1, 1) == u8"b"); @@ -306,6 +316,7 @@ CHECK(makeStdString(malformed) == u8""); } { + CHECK(makeStdString((NSString *)nullptr) == U""); CHECK(makeStdString(@"abc") == U"abc"); CHECK(makeStdString(@"abc", 1) == U"bc"); CHECK(makeStdString(@"abc", 1, 1) == U"b"); @@ -314,6 +325,7 @@ CHECK(makeStdString(malformed) == U""); } { + CHECK(makeStdString((NSString *)nullptr) == L""); CHECK(makeStdString(@"abc") == L"abc"); CHECK(makeStdString(@"abc", 1) == L"bc"); CHECK(makeStdString(@"abc", 1, 1) == L"b"); diff --git a/test/NSStringUtilTestsCpp.cpp b/test/NSStringUtilTestsCpp.cpp index 3f3f988..9ae275e 100644 --- a/test/NSStringUtilTestsCpp.cpp +++ b/test/NSStringUtilTestsCpp.cpp @@ -32,6 +32,7 @@ TEST_CASE("makeCFString") { { + CHECK(eq(hold(makeCFString(u"")), CFSTR(""))); CHECK(eq(hold(makeCFString(u"abc")), CFSTR("abc"))); CHECK(eq(hold(makeCFString(u"abc"s)), CFSTR("abc"))); CHECK(eq(hold(makeCFString(u"abc"sv)), CFSTR("abc"))); @@ -55,6 +56,7 @@ TEST_CASE("makeCFString") { } { + CHECK(eq(hold(makeCFString("")), CFSTR(""))); CHECK(eq(hold(makeCFString("abc")), CFSTR("abc"))); const char * str = nullptr; @@ -74,6 +76,7 @@ TEST_CASE("makeCFString") { } { + CHECK(eq(hold(makeCFString(u8"")), CFSTR(""))); CHECK(eq(hold(makeCFString(u8"abc")), CFSTR("abc"))); const char8_t * str = nullptr; @@ -93,6 +96,7 @@ TEST_CASE("makeCFString") { } { + CHECK(eq(hold(makeCFString(u8"")), CFSTR(""))); CHECK(eq(hold(makeCFString(u8"abc")), CFSTR("abc"))); const char8_t * str = nullptr; @@ -112,6 +116,7 @@ TEST_CASE("makeCFString") { } { + CHECK(eq(hold(makeCFString(U"")), CFSTR(""))); CHECK(eq(hold(makeCFString(U"abc")), CFSTR("abc"))); const char32_t * str = nullptr; @@ -131,6 +136,7 @@ TEST_CASE("makeCFString") { } { + CHECK(eq(hold(makeCFString(L"")), CFSTR(""))); CHECK(eq(hold(makeCFString(L"abc")), CFSTR("abc"))); const wchar_t * str = nullptr; @@ -159,41 +165,54 @@ TEST_CASE("makeStdString") { auto malformed = hold(makeCFString(u"\xD800")); { + CHECK(makeStdString((CFStringRef)nullptr) == u""); CHECK(makeStdString(CFSTR("abc")) == u"abc"); CHECK(makeStdString(CFSTR("abc"), 1) == u"bc"); CHECK(makeStdString(CFSTR("abc"), 1, 1) == u"b"); + CHECK(makeStdString(CFSTR("abc"), -1, 1) == u"a"); + CHECK(makeStdString(CFSTR("abc"), 1, 7) == u"bc"); + CHECK(makeStdString(CFSTR("abc"), 3, 5) == u""); + CHECK(makeStdString(CFSTR("abc"), 1, 0) == u""); CHECK(makeStdString(NSStringCharAccess(CFSTR("abc")) | take(2)) == u"ab"); CHECK(makeStdString(access.begin(), access.end()) == u"abc"); CHECK(makeStdString(malformed) == u"\xD800"); } { + CHECK(makeStdString((CFStringRef)nullptr) == ""); CHECK(makeStdString(CFSTR("abc")) == "abc"); CHECK(makeStdString(CFSTR("abc"), 1) == "bc"); CHECK(makeStdString(CFSTR("abc"), 1, 1) == "b"); + CHECK(makeStdString(CFSTR("abc"), -1, 1) == "a"); CHECK(makeStdString(NSStringCharAccess(CFSTR("abc")) | take(2)) == "ab"); CHECK(makeStdString(access.begin(), access.end()) == "abc"); CHECK(makeStdString(malformed) == ""); } { + CHECK(makeStdString((CFStringRef)nullptr) == u8""); CHECK(makeStdString(CFSTR("abc")) == u8"abc"); CHECK(makeStdString(CFSTR("abc"), 1) == u8"bc"); CHECK(makeStdString(CFSTR("abc"), 1, 1) == u8"b"); + CHECK(makeStdString(CFSTR("abc"), -1, 1) == u8"a"); CHECK(makeStdString(NSStringCharAccess(CFSTR("abc")) | take(2)) == u8"ab"); CHECK(makeStdString(access.begin(), access.end()) == u8"abc"); CHECK(makeStdString(malformed) == u8""); } { + CHECK(makeStdString((CFStringRef)nullptr) == U""); CHECK(makeStdString(CFSTR("abc")) == U"abc"); CHECK(makeStdString(CFSTR("abc"), 1) == U"bc"); CHECK(makeStdString(CFSTR("abc"), 1, 1) == U"b"); + CHECK(makeStdString(CFSTR("abc"), -1, 1) == U"a"); CHECK(makeStdString(NSStringCharAccess(CFSTR("abc")) | take(2)) == U"ab"); CHECK(makeStdString(access.begin(), access.end()) == U"abc"); CHECK(makeStdString(malformed) == U""); } { + CHECK(makeStdString((CFStringRef)nullptr) == L""); CHECK(makeStdString(CFSTR("abc")) == L"abc"); CHECK(makeStdString(CFSTR("abc"), 1) == L"bc"); CHECK(makeStdString(CFSTR("abc"), 1, 1) == L"b"); + CHECK(makeStdString(CFSTR("abc"), -1, 1) == L"a"); CHECK(makeStdString(NSStringCharAccess(CFSTR("abc")) | take(2)) == L"ab"); CHECK(makeStdString(access.begin(), access.end()) == L"abc"); CHECK(makeStdString(malformed) == L"");