From e2dc4fb341bb8365d16961fc7108a7452a11b891 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Wed, 10 Oct 2018 20:50:34 -0700 Subject: [PATCH] Implement SE-0221: Character Properties Provide convenience properties on Character. --- stdlib/public/core/CMakeLists.txt | 1 + stdlib/public/core/CharacterProperties.swift | 293 ++++++++++++++++++ stdlib/public/core/GroupInfo.json | 1 + .../public/core/UnicodeScalarProperties.swift | 20 ++ test/stdlib/CharacterProperties.swift | 191 ++++++++++++ .../stdlib/CharacterPropertiesLong.swift | 74 +++++ 6 files changed, 580 insertions(+) create mode 100644 stdlib/public/core/CharacterProperties.swift create mode 100644 test/stdlib/CharacterProperties.swift create mode 100644 validation-test/stdlib/CharacterPropertiesLong.swift diff --git a/stdlib/public/core/CMakeLists.txt b/stdlib/public/core/CMakeLists.txt index 035381d0fa4ab..8370afe7f06de 100644 --- a/stdlib/public/core/CMakeLists.txt +++ b/stdlib/public/core/CMakeLists.txt @@ -170,6 +170,7 @@ set(SWIFTLIB_ESSENTIAL UnicodeEncoding.swift UnicodeParser.swift UnicodeScalarProperties.swift + CharacterProperties.swift # ORDER DEPENDENCY: UnicodeScalarProperties.swift Unmanaged.swift UnmanagedOpaqueString.swift UnmanagedString.swift diff --git a/stdlib/public/core/CharacterProperties.swift b/stdlib/public/core/CharacterProperties.swift new file mode 100644 index 0000000000000..21f33a9cfa22f --- /dev/null +++ b/stdlib/public/core/CharacterProperties.swift @@ -0,0 +1,293 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +extension Character { + @inlinable + internal var _firstScalar: Unicode.Scalar { + return self.unicodeScalars.first! + } + @inlinable + internal var _isSingleScalar: Bool { + return self.unicodeScalars.count == 1 + } + + @inlinable + static internal var _crlf: Character { return "\r\n" } + + @inlinable + static internal var _lf: Character { return "\n" } + + /// Whether this Character is ASCII. + @inlinable + public var isASCII: Bool { + return asciiValue != nil + } + + /// Returns the ASCII encoding value of this Character, if ASCII. + /// + /// Note: "\r\n" (CR-LF) is normalized to "\n" (LF), which will return 0x0A + @inlinable + public var asciiValue: UInt8? { + if _slowPath(self == ._crlf) { return 0x000A /* LINE FEED (LF) */ } + if _slowPath(!_isSingleScalar || _firstScalar.value >= 0x80) { return nil } + return UInt8(_firstScalar.value) + } + + /// Whether this Character represents whitespace, including newlines. + /// + /// Examples: + /// * "\t" (U+0009 CHARACTER TABULATION) + /// * " " (U+0020 SPACE) + /// * U+2029 PARAGRAPH SEPARATOR + /// * U+3000 IDEOGRAPHIC SPACE + /// + public var isWhitespace: Bool { + return _firstScalar.properties.isWhitespace + } + + /// Whether this Character represents a newline. + /// + /// Examples: + /// * "\n" (U+000A): LINE FEED (LF) + /// * U+000B: LINE TABULATION (VT) + /// * U+000C: FORM FEED (FF) + /// * "\r" (U+000D): CARRIAGE RETURN (CR) + /// * "\r\n" (U+000A U+000D): CR-LF + /// * U+0085: NEXT LINE (NEL) + /// * U+2028: LINE SEPARATOR + /// * U+2029: PARAGRAPH SEPARATOR + /// + @inlinable + public var isNewline: Bool { + switch _firstScalar.value { + case 0x000A...0x000D /* LF ... CR */: return true + case 0x0085 /* NEXT LINE (NEL) */: return true + case 0x2028 /* LINE SEPARATOR */: return true + case 0x2029 /* PARAGRAPH SEPARATOR */: return true + default: return false + } + } + + /// Whether this Character represents a number. + /// + /// Examples: + /// * "7" (U+0037 DIGIT SEVEN) + /// * "⅚" (U+215A VULGAR FRACTION FIVE SIXTHS) + /// * "㊈" (U+3288 CIRCLED IDEOGRAPH NINE) + /// * "𝟠" (U+1D7E0 MATHEMATICAL DOUBLE-STRUCK DIGIT EIGHT) + /// * "๒" (U+0E52 THAI DIGIT TWO) + /// + public var isNumber: Bool { + return _firstScalar.properties.numericType != nil + } + + /// Whether this Character represents a whole number. See + /// `Character.wholeNumberValue` + @inlinable + public var isWholeNumber: Bool { + return wholeNumberValue != nil + } + + /// If this Character is a whole number, return the value it represents, else + /// nil. + /// + /// Examples: + /// * "1" (U+0031 DIGIT ONE) => 1 + /// * "५" (U+096B DEVANAGARI DIGIT FIVE) => 5 + /// * "๙" (U+0E59 THAI DIGIT NINE) => 9 + /// * "万" (U+4E07 CJK UNIFIED IDEOGRAPH-4E07) => 10_000 + /// + /// Note: Returns nil on 32-bit platforms if the result would overflow `Int`. + public var wholeNumberValue: Int? { + guard _isSingleScalar else { return nil } + guard let value = _firstScalar.properties.numericValue else { return nil } + return Int(exactly: value) + } + + /// Whether this Character represents a hexadecimal digit. + /// + /// Hexadecimal digits include 0-9, Latin letters a-f and A-F, and their + /// fullwidth compatibility forms. To get their value, see + /// `Character.hexDigitValue` + @inlinable + public var isHexDigit: Bool { + return hexDigitValue != nil + } + + /// If this Character is a hexadecimal digit, returns the value it represents, + /// else nil. + public var hexDigitValue: Int? { + guard _isSingleScalar else { return nil } + let value = _firstScalar.value + switch value { + // DIGIT ZERO..DIGIT NINE + case 0x0030...0x0039: return Int(value &- 0x0030) + // LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER F + case 0x0041...0x0046: return Int((value &+ 10) &- 0x0041) + // LATIN SMALL LETTER A..LATIN SMALL LETTER F + case 0x0061...0x0066: return Int((value &+ 10) &- 0x0061) + // FULLWIDTH DIGIT ZERO..FULLWIDTH DIGIT NINE + case 0xFF10...0xFF19: return Int(value &- 0xFF10) + // FULLWIDTH LATIN CAPITAL LETTER A..FULLWIDTH LATIN CAPITAL LETTER F + case 0xFF21...0xFF26: return Int((value &+ 10) &- 0xFF21) + // FULLWIDTH LATIN SMALL LETTER A..FULLWIDTH LATIN SMALL LETTER F + case 0xFF41...0xFF46: return Int((value &+ 10) &- 0xFF41) + + default: return nil + } + } + + /// Whether this Character is a letter. + /// + /// Examples: + /// * "A" (U+0041 LATIN CAPITAL LETTER A) + /// * "é" (U+0065 LATIN SMALL LETTER E, U+0301 COMBINING ACUTE ACCENT) + /// * "ϴ" (U+03F4 GREEK CAPITAL THETA SYMBOL) + /// * "ڈ" (U+0688 ARABIC LETTER DDAL) + /// * "日" (U+65E5 CJK UNIFIED IDEOGRAPH-65E5) + /// * "ᚨ" (U+16A8 RUNIC LETTER ANSUZ A) + /// + public var isLetter: Bool { + return _firstScalar.properties.isAlphabetic + } + + /// Perform case conversion to uppercase + /// + /// Examples: + /// * "é" (U+0065 LATIN SMALL LETTER E, U+0301 COMBINING ACUTE ACCENT) + /// => "É" (U+0045 LATIN CAPITAL LETTER E, U+0301 COMBINING ACUTE ACCENT) + /// * "и" (U+0438 CYRILLIC SMALL LETTER I) + /// => "И" (U+0418 CYRILLIC CAPITAL LETTER I) + /// * "π" (U+03C0 GREEK SMALL LETTER PI) + /// => "Π" (U+03A0 GREEK CAPITAL LETTER PI) + /// * "ß" (U+00DF LATIN SMALL LETTER SHARP S) + /// => "SS" (U+0053 LATIN CAPITAL LETTER S, U+0053 LATIN CAPITAL LETTER S) + /// + /// Note: Returns a String as case conversion can result in multiple + /// Characters. + public func uppercased() -> String { return String(self).uppercased() } + + /// Perform case conversion to lowercase + /// + /// Examples: + /// * "É" (U+0045 LATIN CAPITAL LETTER E, U+0301 COMBINING ACUTE ACCENT) + /// => "é" (U+0065 LATIN SMALL LETTER E, U+0301 COMBINING ACUTE ACCENT) + /// * "И" (U+0418 CYRILLIC CAPITAL LETTER I) + /// => "и" (U+0438 CYRILLIC SMALL LETTER I) + /// * "Π" (U+03A0 GREEK CAPITAL LETTER PI) + /// => "π" (U+03C0 GREEK SMALL LETTER PI) + /// + /// Note: Returns a String as case conversion can result in multiple + /// Characters. + public func lowercased() -> String { return String(self).lowercased() } + + @usableFromInline + internal var _isUppercased: Bool { return String(self) == self.uppercased() } + @usableFromInline + internal var _isLowercased: Bool { return String(self) == self.lowercased() } + + /// Whether this Character is considered uppercase. + /// + /// Uppercase Characters vary under case-conversion to lowercase, but not when + /// converted to uppercase. + /// + /// Examples: + /// * "É" (U+0045 LATIN CAPITAL LETTER E, U+0301 COMBINING ACUTE ACCENT) + /// * "И" (U+0418 CYRILLIC CAPITAL LETTER I) + /// * "Π" (U+03A0 GREEK CAPITAL LETTER PI) + /// + @inlinable + public var isUppercase: Bool { + if _fastPath(_isSingleScalar && _firstScalar.properties.isUppercase) { + return true + } + return _isUppercased && isCased + } + + /// Whether this Character is considered lowercase. + /// + /// Lowercase Characters vary under case-conversion to uppercase, but not when + /// converted to lowercase. + /// + /// Examples: + /// * "é" (U+0065 LATIN SMALL LETTER E, U+0301 COMBINING ACUTE ACCENT) + /// * "и" (U+0438 CYRILLIC SMALL LETTER I) + /// * "π" (U+03C0 GREEK SMALL LETTER PI) + /// + @inlinable + public var isLowercase: Bool { + if _fastPath(_isSingleScalar && _firstScalar.properties.isLowercase) { + return true + } + return _isLowercased && isCased + } + + /// Whether this Character changes under any form of case conversion. + @inlinable + public var isCased: Bool { + if _fastPath(_isSingleScalar && _firstScalar.properties.isCased) { + return true + } + return !_isUppercased || !_isLowercased + } + + /// Whether this Character represents a symbol + /// + /// Examples: + /// * "®" (U+00AE REGISTERED SIGN) + /// * "⌹" (U+2339 APL FUNCTIONAL SYMBOL QUAD DIVIDE) + /// * "⡆" (U+2846 BRAILLE PATTERN DOTS-237) + /// + public var isSymbol: Bool { + return _firstScalar.properties.generalCategory._isSymbol + } + + /// Whether this Character represents a symbol used mathematical formulas + /// + /// Examples: + /// * "+" (U+002B PLUS SIGN) + /// * "∫" (U+222B INTEGRAL) + /// * "ϰ" (U+03F0 GREEK KAPPA SYMBOL) + /// + /// Note: This is not a strict subset of isSymbol. This includes characters + /// used both as letters and commonly in mathematical formulas. For example, + /// "ϰ" (U+03F0 GREEK KAPPA SYMBOL) is considered a both mathematical symbol + /// and a letter. + /// + public var isMathSymbol: Bool { + return _firstScalar.properties.isMath + } + + /// Whether this Character represents a currency symbol + /// + /// Examples: + /// * "$" (U+0024 DOLLAR SIGN) + /// * "¥" (U+00A5 YEN SIGN) + /// * "€" (U+20AC EURO SIGN) + /// + public var isCurrencySymbol: Bool { + return _firstScalar.properties.generalCategory == .currencySymbol + } + + /// Whether this Character represents punctuation + /// + /// Examples: + /// * "!" (U+0021 EXCLAMATION MARK) + /// * "؟" (U+061F ARABIC QUESTION MARK) + /// * "…" (U+2026 HORIZONTAL ELLIPSIS) + /// * "—" (U+2014 EM DASH) + /// * "“" (U+201C LEFT DOUBLE QUOTATION MARK) + /// + public var isPunctuation: Bool { + return _firstScalar.properties.generalCategory._isPunctuation + } +} diff --git a/stdlib/public/core/GroupInfo.json b/stdlib/public/core/GroupInfo.json index f842d7d741d63..1a4878cf66122 100644 --- a/stdlib/public/core/GroupInfo.json +++ b/stdlib/public/core/GroupInfo.json @@ -7,6 +7,7 @@ "ASCII.swift", "CString.swift", "Character.swift", + "CharacterProperties.swift", "CharacterUnicodeScalars.swift", "ICU.swift", "NormalizedCodeUnitIterator.swift", diff --git a/stdlib/public/core/UnicodeScalarProperties.swift b/stdlib/public/core/UnicodeScalarProperties.swift index 756a355d81a83..fe60238b9a4b5 100644 --- a/stdlib/public/core/UnicodeScalarProperties.swift +++ b/stdlib/public/core/UnicodeScalarProperties.swift @@ -1066,6 +1066,26 @@ extension Unicode { } } +// Internal helpers +extension Unicode.GeneralCategory { + internal var _isSymbol: Bool { + switch self { + case .mathSymbol, .currencySymbol, .modifierSymbol, .otherSymbol: + return true + default: return false + } + } + internal var _isPunctuation: Bool { + switch self { + case .connectorPunctuation, .dashPunctuation, .openPunctuation, + .closePunctuation, .initialPunctuation, .finalPunctuation, + .otherPunctuation: + return true + default: return false + } + } +} + extension Unicode.Scalar.Properties { /// The general category (most usual classification) of the scalar. diff --git a/test/stdlib/CharacterProperties.swift b/test/stdlib/CharacterProperties.swift new file mode 100644 index 0000000000000..b41e680e59114 --- /dev/null +++ b/test/stdlib/CharacterProperties.swift @@ -0,0 +1,191 @@ +// RUN: %target-run-simple-swift +// REQUIRES: executable_test + +import StdlibUnittest + +var CharacterPropertiesTests = TestSuite("StringTests") + +CharacterPropertiesTests.test("ASCII queries") { + for cu in (0 as UInt32)...(0x7F as UInt32) { + let c = Character(Unicode.Scalar(cu)!) + expectTrue(c.isASCII) + expectEqual(cu, UInt32(c.asciiValue!)) + } +} + +CharacterPropertiesTests.test("Hex queries") { + let hexDigits: Array = [ + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", // 0-9 + "a", "b", "c", "d", "e", "f", // 10-15 + "A", "B", "C", "D", "E", "F", // 16-21 + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", // 22-31 + "A", "B", "C", "D", "E", "F", // 32-37 + "a", "b", "c", "d", "e", "f", // 38-43 + ] + + // Ensure below loop logic is correct with a couple hard-coded checks + expectEqual(1, Character("1").hexDigitValue!) + expectEqual(2, Character("2").hexDigitValue!) + expectEqual(11, Character("B").hexDigitValue!) + expectEqual(12, Character("c").hexDigitValue!) + expectEqual(14, Character("e").hexDigitValue!) + expectEqual(15, Character("f").hexDigitValue!) + + for i in hexDigits.indices { + let hexValue = hexDigits[i].hexDigitValue! + switch i { + case 0...15: expectEqual(i, hexValue) + case 16...21: expectEqual(i-6, hexValue) + case 22...37: expectEqual(i-22, hexValue) + case 38...43: expectEqual(i-28, hexValue) + default: print(i); fatalError("unreachable") + } + } +} + +CharacterPropertiesTests.test("Numbers") { + // Some hard coded tests + expectTrue(Character("⅚").isNumber) + expectTrue(Character("5️⃣").isNumber) + expectTrue(Character("𝟠").isNumber) + expectTrue(Character("㊈").isNumber) + expectTrue(Character("7").isNumber) + expectTrue(Character("𐹾").isNumber) // RUMI FRACTION 2/3 +} + +CharacterPropertiesTests.test("Whole Numbers") { + // Random smattering of hard-coded tests + expectEqual(0, Character("↉").wholeNumberValue) // baseball scoring + expectEqual(0, Character("𝟶").wholeNumberValue) // math monospace + expectEqual(1, Character("1").wholeNumberValue) + expectEqual(1, Character("𒐴").wholeNumberValue) + expectEqual(1, Character("ⅰ").wholeNumberValue) // small roman numeral + expectEqual(1, Character("𝟏").wholeNumberValue) // math bold + expectEqual(2, Character("②").wholeNumberValue) + expectEqual(2, Character("٢").wholeNumberValue) + expectEqual(2, Character("২").wholeNumberValue) + expectEqual(2, Character("੨").wholeNumberValue) + expectEqual(2, Character("૨").wholeNumberValue) + expectEqual(3, Character("³").wholeNumberValue) + expectEqual(4, Character("٤").wholeNumberValue) + expectEqual(4, Character("൪").wholeNumberValue) + expectEqual(4, Character("೪").wholeNumberValue) + expectEqual(4, Character("౪").wholeNumberValue) + expectEqual(4, Character("௪").wholeNumberValue) + expectEqual(4, Character("୪").wholeNumberValue) + expectEqual(4, Character("૪").wholeNumberValue) + expectEqual(4, Character("੪").wholeNumberValue) + expectEqual(4, Character("๔").wholeNumberValue) + expectEqual(4, Character("໔").wholeNumberValue) + expectEqual(5, Character("५").wholeNumberValue) + expectEqual(5, Character("༥").wholeNumberValue) + expectEqual(5, Character("፭").wholeNumberValue) + expectEqual(5, Character("᠕").wholeNumberValue) + expectEqual(5, Character("Ⅴ").wholeNumberValue) // Roman numeral + expectEqual(5, Character("𐌡").wholeNumberValue) + expectEqual(5, Character("߅").wholeNumberValue) + expectEqual(5, Character("᭕").wholeNumberValue) + expectEqual(5, Character("𝍤").wholeNumberValue) + expectEqual(5, Character("᮵").wholeNumberValue) + expectEqual(6, Character("六").wholeNumberValue) + expectEqual(6, Character("六").wholeNumberValue) // Compatibility + expectEqual(7, Character("𝟩").wholeNumberValue) // Math san-serif + expectEqual(7, Character("㈦").wholeNumberValue) + expectEqual(7, Character("㊆").wholeNumberValue) + expectEqual(7, Character("𑁭").wholeNumberValue) + expectEqual(8, Character("꧘").wholeNumberValue) + expectEqual(8, Character("᪈").wholeNumberValue) + expectEqual(8, Character("᪘").wholeNumberValue) + expectEqual(8, Character("꩘").wholeNumberValue) + expectEqual(9, Character("๙").wholeNumberValue) + + expectEqual(18, Character("⒅").wholeNumberValue) + expectEqual(20, Character("⑳").wholeNumberValue) + expectEqual(20, Character("𐄑").wholeNumberValue) + expectEqual(20, Character("𐏔").wholeNumberValue) + expectEqual(20, Character("𐤘").wholeNumberValue) + expectEqual(20, Character("〹").wholeNumberValue) + expectEqual(50, Character("ↆ").wholeNumberValue) + expectEqual(70, Character("𑁡").wholeNumberValue) + expectEqual(90, Character("𐍁").wholeNumberValue) + expectEqual(1_000, Character("𑁥").wholeNumberValue) + expectEqual(5_000, Character("ↁ").wholeNumberValue) + expectEqual(10_000, Character("万").wholeNumberValue) +} + +CharacterPropertiesTests.test("Casing") { + let eAccent = Character("\u{0065}\u{0301}") + let EAccent = Character("\u{0045}\u{0301}") + expectTrue(eAccent.isLowercase && eAccent.isCased) + expectFalse(eAccent.isUppercase) + expectTrue(EAccent.isUppercase && EAccent.isCased) + expectFalse(EAccent.isLowercase) + + expectTrue(Character("И").isUppercase) + expectTrue(Character("и").isLowercase) + expectTrue(Character("Π").isUppercase) + expectTrue(Character("π").isLowercase) + + expectEqual("SS", Character("ß").uppercased()) + expectEqual("и", Character("И").lowercased()) + expectEqual("И", Character("и").uppercased()) + expectEqual("π", Character("Π").lowercased()) + expectEqual("Π", Character("π").uppercased()) +} + +CharacterPropertiesTests.test("Punctuation") { + expectTrue(Character("!").isPunctuation) + expectTrue(Character("؟").isPunctuation) + expectTrue(Character("…").isPunctuation) + expectTrue(Character("—").isPunctuation) + expectTrue(Character("“").isPunctuation) + + expectTrue(Character("﹏").isPunctuation) // compatibility +} + +CharacterPropertiesTests.test("Symbols") { + // Other symbols + expectTrue(Character("🌍").isSymbol) + expectTrue(Character("👽").isSymbol) + expectTrue(Character("®").isSymbol) + expectTrue(Character("⌹").isSymbol) + expectTrue(Character("⡆").isSymbol) + + // Currency + expectTrue(Character("$").isCurrencySymbol) + expectTrue(Character("¥").isCurrencySymbol) + expectTrue(Character("€").isCurrencySymbol) + + // Math symbols + expectTrue(Character("∩").isSymbol) + expectTrue(Character("∩").isMathSymbol) + expectTrue(Character("+").isSymbol) + expectTrue(Character("+").isMathSymbol) + expectTrue(Character("⟺").isSymbol) + expectTrue(Character("⟺").isMathSymbol) + expectTrue(Character("∫").isSymbol) + expectTrue(Character("∫").isMathSymbol) + + // Math symbols that are letters + expectFalse(Character("ϰ").isSymbol) + expectTrue(Character("ϰ").isMathSymbol) +} + +CharacterPropertiesTests.test("Whitespace") { + expectTrue(Character("\t").isWhitespace) + expectTrue(Character(" ").isWhitespace) + expectTrue(Character("\u{2029}").isWhitespace) + expectTrue(Character("\u{3000}").isWhitespace) +} + +CharacterPropertiesTests.test("Newline") { + expectTrue(Character("\n").isNewline) + expectTrue(Character("\r").isNewline) + expectTrue(Character("\r\n").isNewline) + expectTrue(Character("\u{0085}").isNewline) + expectTrue(Character("\u{2028}").isNewline) + expectTrue(Character("\u{2029}").isNewline) +} + +runAllTests() + diff --git a/validation-test/stdlib/CharacterPropertiesLong.swift b/validation-test/stdlib/CharacterPropertiesLong.swift new file mode 100644 index 0000000000000..421db14802094 --- /dev/null +++ b/validation-test/stdlib/CharacterPropertiesLong.swift @@ -0,0 +1,74 @@ +// RUN: %target-run-simple-swift +// REQUIRES: executable_test + +import StdlibUnittest + +var CharacterPropertiesTests = TestSuite("StringTests") + +func genericPropertyValidation(_ c: Character) { + // Newline is subset of whitespace + if c.isNewline { + expectTrue(c.isWhitespace) + } + + // Currency symbol is subset of symbols + if c.isCurrencySymbol { + expectTrue(c.isSymbol) + } + + // Whole number is subset of number + if c.isWholeNumber { + expectTrue(c.isNumber) + let value = c.wholeNumberValue + expectNotNil(value) + if let hexValue = c.hexDigitValue { + expectEqual(value, hexValue) + } + } + + // Hex includes letters like a-f + if c.isHexDigit { + let value = c.hexDigitValue + expectNotNil(value) + + // For hex digits, number and letter are mutually exclusive + if c.isWholeNumber { + expectTrue(value! <= 9 && value! >= 0) + expectTrue(c.isNumber) + expectFalse(c.isLetter) + } else { + expectTrue(c.isLetter) + expectFalse(c.isNumber) + let value = c.hexDigitValue + expectTrue(value! >= 10 && value! <= 15) + } + } + + if c.isUppercase || c.isLowercase { + expectTrue(c.isCased) + if c.isUppercase { + expectEqual(c, c.uppercased().first!) + } + if c.isLowercase { + expectEqual(c, c.lowercased().first!) + } + } + + // TODO: Any thing else we can check in the abstract? +} + +CharacterPropertiesTests.test("Broad grapheme checks") { + for cu in (0 as UInt32)...(0x10FFFF as UInt32) { + guard let scalar = Unicode.Scalar(cu) else { continue } + let c = Character(scalar) + genericPropertyValidation(c) + + // Add a modifying scalar on the end + let str = String(scalar) + "\u{301}" + guard str.count == 1 else { continue } + genericPropertyValidation(Character(str)) + } +} + +runAllTests() +