diff --git a/include/fmt/format.h b/include/fmt/format.h index d74c3e93c14d..f87492d14886 100644 --- a/include/fmt/format.h +++ b/include/fmt/format.h @@ -649,26 +649,139 @@ FMT_CONSTEXPR void for_each_codepoint(string_view s, F f) { } while (buf_ptr < buf + num_chars_left); } +struct WidthRange { + uint32_t first; + uint32_t last; + uint8_t width; +}; + +constexpr WidthRange kUnicodeWidthRanges[] = { + {0x1100, 0x115F, 2}, {0x231A, 0x231B, 2}, {0x2329, 0x2329, 2}, + {0x232A, 0x232A, 2}, {0x23E9, 0x23EC, 2}, {0x23F0, 0x23F0, 2}, + {0x23F3, 0x23F3, 2}, {0x25FD, 0x25FE, 2}, {0x2614, 0x2615, 2}, + {0x2630, 0x2637, 2}, {0x2648, 0x2653, 2}, {0x267F, 0x267F, 2}, + {0x268A, 0x268F, 2}, {0x2693, 0x2693, 2}, {0x26A1, 0x26A1, 2}, + {0x26AA, 0x26AB, 2}, {0x26BD, 0x26BE, 2}, {0x26C4, 0x26C5, 2}, + {0x26CE, 0x26CE, 2}, {0x26D4, 0x26D4, 2}, {0x26EA, 0x26EA, 2}, + {0x26F2, 0x26F3, 2}, {0x26F5, 0x26F5, 2}, {0x26FA, 0x26FA, 2}, + {0x26FD, 0x26FD, 2}, {0x2705, 0x2705, 2}, {0x270A, 0x270B, 2}, + {0x2728, 0x2728, 2}, {0x274C, 0x274C, 2}, {0x274E, 0x274E, 2}, + {0x2753, 0x2755, 2}, {0x2757, 0x2757, 2}, {0x2795, 0x2797, 2}, + {0x27B0, 0x27B0, 2}, {0x27BF, 0x27BF, 2}, {0x2B1B, 0x2B1C, 2}, + {0x2B50, 0x2B50, 2}, {0x2B55, 0x2B55, 2}, {0x2E80, 0x2E99, 2}, + {0x2E9B, 0x2EF3, 2}, {0x2F00, 0x2FD5, 2}, {0x2FF0, 0x2FFF, 2}, + {0x3000, 0x3000, 2}, {0x3001, 0x3003, 2}, {0x3004, 0x3004, 2}, + {0x3005, 0x3005, 2}, {0x3006, 0x3006, 2}, {0x3007, 0x3007, 2}, + {0x3008, 0x3008, 2}, {0x3009, 0x3009, 2}, {0x300A, 0x300A, 2}, + {0x300B, 0x300B, 2}, {0x300C, 0x300C, 2}, {0x300D, 0x300D, 2}, + {0x300E, 0x300E, 2}, {0x300F, 0x300F, 2}, {0x3010, 0x3010, 2}, + {0x3011, 0x3011, 2}, {0x3012, 0x3013, 2}, {0x3014, 0x3014, 2}, + {0x3015, 0x3015, 2}, {0x3016, 0x3016, 2}, {0x3017, 0x3017, 2}, + {0x3018, 0x3018, 2}, {0x3019, 0x3019, 2}, {0x301A, 0x301A, 2}, + {0x301B, 0x301B, 2}, {0x301C, 0x301C, 2}, {0x301D, 0x301D, 2}, + {0x301E, 0x301F, 2}, {0x3020, 0x3020, 2}, {0x3021, 0x3029, 2}, + {0x302A, 0x302D, 2}, {0x302E, 0x302F, 2}, {0x3030, 0x3030, 2}, + {0x3031, 0x3035, 2}, {0x3036, 0x3037, 2}, {0x3038, 0x303A, 2}, + {0x303B, 0x303B, 2}, {0x303C, 0x303C, 2}, {0x303D, 0x303D, 2}, + {0x303E, 0x303E, 2}, {0x3041, 0x3096, 2}, {0x3099, 0x309A, 2}, + {0x309B, 0x309C, 2}, {0x309D, 0x309E, 2}, {0x309F, 0x309F, 2}, + {0x30A0, 0x30A0, 2}, {0x30A1, 0x30FA, 2}, {0x30FB, 0x30FB, 2}, + {0x30FC, 0x30FE, 2}, {0x30FF, 0x30FF, 2}, {0x3105, 0x312F, 2}, + {0x3131, 0x318E, 2}, {0x3190, 0x3191, 2}, {0x3192, 0x3195, 2}, + {0x3196, 0x319F, 2}, {0x31A0, 0x31BF, 2}, {0x31C0, 0x31E5, 2}, + {0x31EF, 0x31EF, 2}, {0x31F0, 0x31FF, 2}, {0x3200, 0x321E, 2}, + {0x3220, 0x3229, 2}, {0x322A, 0x3247, 2}, {0x3250, 0x3250, 2}, + {0x3251, 0x325F, 2}, {0x3260, 0x327F, 2}, {0x3280, 0x3289, 2}, + {0x328A, 0x32B0, 2}, {0x32B1, 0x32BF, 2}, {0x32C0, 0x32FF, 2}, + {0x3300, 0x33FF, 2}, {0x3400, 0x4DBF, 2}, {0x4DC0, 0x4DFF, 2}, + {0x4E00, 0x9FFF, 2}, {0xA000, 0xA014, 2}, {0xA015, 0xA015, 2}, + {0xA016, 0xA48C, 2}, {0xA490, 0xA4C6, 2}, {0xA960, 0xA97C, 2}, + {0xAC00, 0xD7A3, 2}, {0xF900, 0xFA6D, 2}, {0xFA6E, 0xFA6F, 2}, + {0xFA70, 0xFAD9, 2}, {0xFADA, 0xFAFF, 2}, {0xFE10, 0xFE16, 2}, + {0xFE17, 0xFE17, 2}, {0xFE18, 0xFE18, 2}, {0xFE19, 0xFE19, 2}, + {0xFE30, 0xFE30, 2}, {0xFE31, 0xFE32, 2}, {0xFE33, 0xFE34, 2}, + {0xFE35, 0xFE35, 2}, {0xFE36, 0xFE36, 2}, {0xFE37, 0xFE37, 2}, + {0xFE38, 0xFE38, 2}, {0xFE39, 0xFE39, 2}, {0xFE3A, 0xFE3A, 2}, + {0xFE3B, 0xFE3B, 2}, {0xFE3C, 0xFE3C, 2}, {0xFE3D, 0xFE3D, 2}, + {0xFE3E, 0xFE3E, 2}, {0xFE3F, 0xFE3F, 2}, {0xFE40, 0xFE40, 2}, + {0xFE41, 0xFE41, 2}, {0xFE42, 0xFE42, 2}, {0xFE43, 0xFE43, 2}, + {0xFE44, 0xFE44, 2}, {0xFE45, 0xFE46, 2}, {0xFE47, 0xFE47, 2}, + {0xFE48, 0xFE48, 2}, {0xFE49, 0xFE4C, 2}, {0xFE4D, 0xFE4F, 2}, + {0xFE50, 0xFE52, 2}, {0xFE54, 0xFE57, 2}, {0xFE58, 0xFE58, 2}, + {0xFE59, 0xFE59, 2}, {0xFE5A, 0xFE5A, 2}, {0xFE5B, 0xFE5B, 2}, + {0xFE5C, 0xFE5C, 2}, {0xFE5D, 0xFE5D, 2}, {0xFE5E, 0xFE5E, 2}, + {0xFE5F, 0xFE61, 2}, {0xFE62, 0xFE62, 2}, {0xFE63, 0xFE63, 2}, + {0xFE64, 0xFE66, 2}, {0xFE68, 0xFE68, 2}, {0xFE69, 0xFE69, 2}, + {0xFE6A, 0xFE6B, 2}, {0xFF01, 0xFF03, 2}, {0xFF04, 0xFF04, 2}, + {0xFF05, 0xFF07, 2}, {0xFF08, 0xFF08, 2}, {0xFF09, 0xFF09, 2}, + {0xFF0A, 0xFF0A, 2}, {0xFF0B, 0xFF0B, 2}, {0xFF0C, 0xFF0C, 2}, + {0xFF0D, 0xFF0D, 2}, {0xFF0E, 0xFF0F, 2}, {0xFF10, 0xFF19, 2}, + {0xFF1A, 0xFF1B, 2}, {0xFF1C, 0xFF1E, 2}, {0xFF1F, 0xFF20, 2}, + {0xFF21, 0xFF3A, 2}, {0xFF3B, 0xFF3B, 2}, {0xFF3C, 0xFF3C, 2}, + {0xFF3D, 0xFF3D, 2}, {0xFF3E, 0xFF3E, 2}, {0xFF3F, 0xFF3F, 2}, + {0xFF40, 0xFF40, 2}, {0xFF41, 0xFF5A, 2}, {0xFF5B, 0xFF5B, 2}, + {0xFF5C, 0xFF5C, 2}, {0xFF5D, 0xFF5D, 2}, {0xFF5E, 0xFF5E, 2}, + {0xFF5F, 0xFF5F, 2}, {0xFF60, 0xFF60, 2}, {0xFFE0, 0xFFE1, 2}, + {0xFFE2, 0xFFE2, 2}, {0xFFE3, 0xFFE3, 2}, {0xFFE4, 0xFFE4, 2}, + {0xFFE5, 0xFFE6, 2}, {0x16FE0, 0x16FE1, 2}, {0x16FE2, 0x16FE2, 2}, + {0x16FE3, 0x16FE3, 2}, {0x16FE4, 0x16FE4, 2}, {0x16FF0, 0x16FF1, 2}, + {0x16FF2, 0x16FF3, 2}, {0x16FF4, 0x16FF6, 2}, {0x17000, 0x187FF, 2}, + {0x18800, 0x18AFF, 2}, {0x18B00, 0x18CD5, 2}, {0x18CFF, 0x18CFF, 2}, + {0x18D00, 0x18D1E, 2}, {0x18D80, 0x18DF2, 2}, {0x1AFF0, 0x1AFF3, 2}, + {0x1AFF5, 0x1AFFB, 2}, {0x1AFFD, 0x1AFFE, 2}, {0x1B000, 0x1B0FF, 2}, + {0x1B100, 0x1B122, 2}, {0x1B132, 0x1B132, 2}, {0x1B150, 0x1B152, 2}, + {0x1B155, 0x1B155, 2}, {0x1B164, 0x1B167, 2}, {0x1B170, 0x1B2FB, 2}, + {0x1D300, 0x1D356, 2}, {0x1D360, 0x1D376, 2}, {0x1F004, 0x1F004, 2}, + {0x1F0CF, 0x1F0CF, 2}, {0x1F18E, 0x1F18E, 2}, {0x1F191, 0x1F19A, 2}, + {0x1F200, 0x1F202, 2}, {0x1F210, 0x1F23B, 2}, {0x1F240, 0x1F248, 2}, + {0x1F250, 0x1F251, 2}, {0x1F260, 0x1F265, 2}, {0x1F300, 0x1F320, 2}, + {0x1F32D, 0x1F335, 2}, {0x1F337, 0x1F37C, 2}, {0x1F37E, 0x1F393, 2}, + {0x1F3A0, 0x1F3CA, 2}, {0x1F3CF, 0x1F3D3, 2}, {0x1F3E0, 0x1F3F0, 2}, + {0x1F3F4, 0x1F3F4, 2}, {0x1F3F8, 0x1F3FA, 2}, {0x1F3FB, 0x1F3FF, 2}, + {0x1F400, 0x1F43E, 2}, {0x1F440, 0x1F440, 2}, {0x1F442, 0x1F4FC, 2}, + {0x1F4FF, 0x1F53D, 2}, {0x1F54B, 0x1F54E, 2}, {0x1F550, 0x1F567, 2}, + {0x1F57A, 0x1F57A, 2}, {0x1F595, 0x1F596, 2}, {0x1F5A4, 0x1F5A4, 2}, + {0x1F5FB, 0x1F5FF, 2}, {0x1F600, 0x1F64F, 2}, {0x1F680, 0x1F6C5, 2}, + {0x1F6CC, 0x1F6CC, 2}, {0x1F6D0, 0x1F6D2, 2}, {0x1F6D5, 0x1F6D8, 2}, + {0x1F6DC, 0x1F6DF, 2}, {0x1F6EB, 0x1F6EC, 2}, {0x1F6F4, 0x1F6FC, 2}, + {0x1F7E0, 0x1F7EB, 2}, {0x1F7F0, 0x1F7F0, 2}, {0x1F90C, 0x1F93A, 2}, + {0x1F93C, 0x1F945, 2}, {0x1F947, 0x1F9FF, 2}, {0x1FA70, 0x1FA7C, 2}, + {0x1FA80, 0x1FA8A, 2}, {0x1FA8E, 0x1FAC6, 2}, {0x1FAC8, 0x1FAC8, 2}, + {0x1FACD, 0x1FADC, 2}, {0x1FADF, 0x1FAEA, 2}, {0x1FAEF, 0x1FAF8, 2}, + {0x20000, 0x2A6DF, 2}, {0x2A6E0, 0x2A6FF, 2}, {0x2A700, 0x2B81D, 2}, + {0x2B81E, 0x2B81F, 2}, {0x2B820, 0x2CEAD, 2}, {0x2CEAE, 0x2CEAF, 2}, + {0x2CEB0, 0x2EBE0, 2}, {0x2EBE1, 0x2EBEF, 2}, {0x2EBF0, 0x2EE5D, 2}, + {0x2EE5E, 0x2F7FF, 2}, {0x2F800, 0x2FA1D, 2}, {0x2FA1E, 0x2FA1F, 2}, + {0x2FA20, 0x2FFFD, 2}, {0x30000, 0x3134A, 2}, {0x3134B, 0x3134F, 2}, + {0x31350, 0x33479, 2}, {0x3347A, 0x3FFFD, 2}, +}; + +constexpr size_t kUnicodeWidthRangeCount = + sizeof(kUnicodeWidthRanges) / sizeof(kUnicodeWidthRanges[0]); + +FMT_CONSTEXPR inline auto is_wide(uint32_t cp) noexcept -> bool { + size_t left = 0; + size_t right = kUnicodeWidthRangeCount; + + while (left < right) { + size_t mid = left + (right - left) / 2; + + const auto& range = kUnicodeWidthRanges[mid]; + + if (cp < range.first) { + right = mid; + } else if (cp > range.last) { + left = mid + 1; + } else { + return true; + } + } + + return false; +} + FMT_CONSTEXPR inline auto display_width_of(uint32_t cp) noexcept -> size_t { - return to_unsigned( - 1 + (cp >= 0x1100 && - (cp <= 0x115f || // Hangul Jamo init. consonants - cp == 0x2329 || // LEFT-POINTING ANGLE BRACKET - cp == 0x232a || // RIGHT-POINTING ANGLE BRACKET - // CJK ... Yi except IDEOGRAPHIC HALF FILL SPACE: - (cp >= 0x2e80 && cp <= 0xa4cf && cp != 0x303f) || - (cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables - (cp >= 0xf900 && cp <= 0xfaff) || // CJK Compatibility Ideographs - (cp >= 0xfe10 && cp <= 0xfe19) || // Vertical Forms - (cp >= 0xfe30 && cp <= 0xfe6f) || // CJK Compatibility Forms - (cp >= 0xff00 && cp <= 0xff60) || // Fullwidth Forms - (cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth Forms - (cp >= 0x20000 && cp <= 0x2fffd) || // CJK - (cp >= 0x30000 && cp <= 0x3fffd) || - // Miscellaneous Symbols and Pictographs + Emoticons: - (cp >= 0x1f300 && cp <= 0x1f64f) || - // Supplemental Symbols and Pictographs: - (cp >= 0x1f900 && cp <= 0x1f9ff)))); + return to_unsigned(1 + is_wide(cp)); } template struct is_integral : std::is_integral {}; diff --git a/test/unicode-test.cc b/test/unicode-test.cc index 58c5d03e8931..25eb078a9168 100644 --- a/test/unicode-test.cc +++ b/test/unicode-test.cc @@ -17,6 +17,26 @@ using testing::Contains; TEST(unicode_test, use_utf8) { EXPECT_TRUE(fmt::detail::use_utf8); } +TEST(unicode_test, unicode_alignment_regression) { + EXPECT_EQ(fmt::format("|{:^20}|", "โŒ"), + "| โŒ |"); + + EXPECT_EQ(fmt::format("|{:^20}|", "โœ…"), + "| โœ… |"); + + EXPECT_EQ(fmt::format("|{:^20}|", "๐Ÿ˜€"), + "| ๐Ÿ˜€ |"); + + EXPECT_EQ(fmt::format("|{:^20}|", "๐Ÿ‡บ๐Ÿ‡ธ"), + "| ๐Ÿ‡บ๐Ÿ‡ธ |"); + + EXPECT_EQ(fmt::format("|{:^20}|", "Mรผller"), + "| Mรผller |"); + + EXPECT_EQ(fmt::format("|{:^20}|", "ๆˆ‘"), + "| ๆˆ‘ |"); +} + TEST(unicode_test, legacy_locale) { auto loc = get_locale("be_BY.CP1251", "Belarusian_Belarus.1251"); if (loc == std::locale::classic()) return;