Add NSMutableAttributedString.truncateHead(until:offset:) method

This commit is contained in:
1024jp 2023-01-07 10:29:48 +09:00
parent cd9329f531
commit f02e0c1ad8
4 changed files with 89 additions and 31 deletions

View File

@ -77,6 +77,31 @@ extension NSAttributedString {
extension NSMutableAttributedString {
/// Truncate head with an ellipsis symbol until the specific `location` if the length before the location is the longer than the `maxOffset`.
///
/// - Parameters:
/// - location: The character index to start truncation.
/// - offset: The maximum number of composed characters to leave on the left of the `location`.
func truncateHead(until location: Int, offset: Int) {
assert(location >= 0)
assert(offset >= 0)
guard location > offset else { return }
let truncationIndex = (self.string as NSString)
.lowerBoundOfComposedCharacterSequence(location, offsetBy: offset)
moof(self.string, location, offset, truncationIndex)
guard truncationIndex > 0 else { return }
self.replaceCharacters(in: NSRange(..<truncationIndex), with: "")
}
}
extension Sequence<NSAttributedString> {
/// Return a new attributed string by concatenating the elements of the sequence, adding the given separator between each element.

View File

@ -311,30 +311,29 @@ extension NSString {
}
/// Return the boundary of the composed character sequence by moving the offset by counting offset in composed character sequences.
/// Return the lower bound of the composed character sequence by moving the bound in the head direction by counting offset in composed character sequences.
///
/// - Parameters:
/// - index: The reference character index in UTF-16.
/// - offset: The number of composed character sequences to move index.
/// - Returns: A character index in UTF-16.
func boundaryOfComposedCharacterSequence(_ index: Int, offsetBy offset: Int) -> Int {
func lowerBoundOfComposedCharacterSequence(_ index: Int, offsetBy offset: Int) -> Int {
assert((0...self.length).contains(index))
assert(offset >= 0)
let reverse = (offset <= 0)
let range = reverse ? NSRange(location: 0, length: min(index + 1, self.length)) : NSRange(location: index, length: self.length - index)
var options: EnumerationOptions = [.byComposedCharacterSequences, .substringNotRequired]
if reverse {
options.formUnion(.reverse)
}
if index == self.length, offset == 0 { return index }
var remainingCount = (index == self.length) ? offset : offset + 1
var boundary = index
var remainingCount = reverse ? -offset + 1 : offset
let range = NSRange(..<min(index + 1, self.length))
let options: EnumerationOptions = [.byComposedCharacterSequences, .substringNotRequired, .reverse]
self.enumerateSubstrings(in: range, options: options) { (_, range, _, stop) in
boundary = reverse ? range.lowerBound : range.upperBound
boundary = range.lowerBound
remainingCount -= 1
if remainingCount <= 0 {
stop.pointee = true
}

View File

@ -8,7 +8,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2020 1024jp
// © 2020-2023 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -53,6 +53,38 @@ final class NSAttributedStringTests: XCTestCase {
}
func testTruncation() throws {
let string1 = NSMutableAttributedString(string: "0123456")
string1.truncateHead(until: 5, offset: 2)
XCTAssertEqual(string1.string, "…3456")
let string2 = NSMutableAttributedString(string: "0123456")
string2.truncateHead(until: 2, offset: 3)
XCTAssertEqual(string2.string, "0123456")
let string3 = NSMutableAttributedString(string: "🐱🐶🐮")
string3.truncateHead(until: 4, offset: 1)
XCTAssertEqual(string3.string, "…🐶🐮")
let string4 = NSMutableAttributedString(string: "🐈‍⬛🐕🐄")
string4.truncateHead(until: 4, offset: 1)
XCTAssertEqual(string4.string, "🐈‍⬛🐕🐄")
let string5 = NSMutableAttributedString(string: "🐈‍⬛🐕🐄")
string5.truncateHead(until: 5, offset: 1)
XCTAssertEqual(string5.string, "🐈‍⬛🐕🐄")
let string6 = NSMutableAttributedString(string: "🐈⬛ab")
string6.truncateHead(until: 5, offset: 1)
XCTAssertEqual(string6.string, "…ab")
let string7 = NSMutableAttributedString(string: "🐈‍⬛🐕🐄")
string7.truncateHead(until: 6, offset: 1)
XCTAssertEqual(string7.string, "…🐕🐄")
}
func testJoin() {
let attrs: [NSAttributedString] = [

View File

@ -9,7 +9,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2015-2022 1024jp
// © 2015-2023 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -311,26 +311,28 @@ final class StringExtensionsTests: XCTestCase {
func testComposedCharacterSequence() {
let blackDog = "🐕‍⬛️" as NSString // 5
XCTAssertEqual(blackDog.boundaryOfComposedCharacterSequence(2, offsetBy: -1), 0)
XCTAssertEqual(blackDog.boundaryOfComposedCharacterSequence(1, offsetBy: 1), blackDog.length)
let blackDog = "🐕‍⬛" // 4
XCTAssertEqual(blackDog.lowerBoundOfComposedCharacterSequence(2, offsetBy: 1), 0)
let string = "🐕🏴‍☠️🇯🇵🧑‍💻" as NSString // 2 5 4 5
XCTAssertEqual(string.boundaryOfComposedCharacterSequence(9, offsetBy: -3), 0)
XCTAssertEqual(string.boundaryOfComposedCharacterSequence(9, offsetBy: -2), 0)
XCTAssertEqual(string.boundaryOfComposedCharacterSequence(9, offsetBy: -1), "🐕".utf16.count)
XCTAssertEqual(string.boundaryOfComposedCharacterSequence(9, offsetBy: 0), "🐕🏴‍☠️".utf16.count)
XCTAssertEqual(string.boundaryOfComposedCharacterSequence(9, offsetBy: 1), "🐕🏴‍☠️🇯🇵".utf16.count)
XCTAssertEqual(string.boundaryOfComposedCharacterSequence(9, offsetBy: 2), "🐕🏴‍☠️🇯🇵🧑‍💻".utf16.count)
XCTAssertEqual(string.boundaryOfComposedCharacterSequence(9, offsetBy: 3), "🐕🏴‍☠️🇯🇵🧑‍💻".utf16.count)
let abcDog = "🐕⬛abc" // 4 1 1 1
XCTAssertEqual(abcDog.lowerBoundOfComposedCharacterSequence(6, offsetBy: 1), "🐕⬛a".utf16.count)
XCTAssertEqual(abcDog.lowerBoundOfComposedCharacterSequence(5, offsetBy: 1), "🐕‍⬛".utf16.count)
let abc = "abc" as NSString
XCTAssertEqual(abc.boundaryOfComposedCharacterSequence(1, offsetBy: -2), 0)
XCTAssertEqual(abc.boundaryOfComposedCharacterSequence(1, offsetBy: -1), 0)
XCTAssertEqual(abc.boundaryOfComposedCharacterSequence(1, offsetBy: 0), 1)
XCTAssertEqual(abc.boundaryOfComposedCharacterSequence(1, offsetBy: 1), 2)
XCTAssertEqual(abc.boundaryOfComposedCharacterSequence(1, offsetBy: 2), 3)
XCTAssertEqual(abc.boundaryOfComposedCharacterSequence(1, offsetBy: 3), 3)
let dogDog = "🐕‍⬛🐕" // 4 2
XCTAssertEqual(dogDog.lowerBoundOfComposedCharacterSequence(5, offsetBy: 1), 0)
XCTAssertEqual(dogDog.lowerBoundOfComposedCharacterSequence(6, offsetBy: 1), "🐕‍⬛".utf16.count)
XCTAssertEqual(dogDog.lowerBoundOfComposedCharacterSequence(6, offsetBy: 0), "🐕‍⬛🐕".utf16.count)
let string = "🐕🏴‍☠️🇯🇵🧑‍💻" // 2 5 4 5
XCTAssertEqual(string.lowerBoundOfComposedCharacterSequence(9, offsetBy: 3), 0)
XCTAssertEqual(string.lowerBoundOfComposedCharacterSequence(9, offsetBy: 2), 0)
XCTAssertEqual(string.lowerBoundOfComposedCharacterSequence(9, offsetBy: 1), "🐕".utf16.count)
XCTAssertEqual(string.lowerBoundOfComposedCharacterSequence(9, offsetBy: 0), "🐕🏴‍☠️".utf16.count)
let abc = "abc"
XCTAssertEqual(abc.lowerBoundOfComposedCharacterSequence(1, offsetBy: 2), 0)
XCTAssertEqual(abc.lowerBoundOfComposedCharacterSequence(1, offsetBy: 1), 0)
XCTAssertEqual(abc.lowerBoundOfComposedCharacterSequence(1, offsetBy: 0), 1)
}