diff --git a/Pachyderm/CharacterCounter.swift b/Pachyderm/CharacterCounter.swift new file mode 100644 index 00000000..709aa4ac --- /dev/null +++ b/Pachyderm/CharacterCounter.swift @@ -0,0 +1,35 @@ +// +// CharacterCounter.swift +// Pachyderm +// +// Created by Shadowfacts on 9/29/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +public struct CharacterCounter { + + static let linkDetector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + static let mention = try! NSRegularExpression(pattern: "(@[a-z0-9_]+)(?:@[a-z0-9\\-\\.]+[a-z0-9]+)?", options: .caseInsensitive) + + public static func count(text: String) -> Int { + let mentionsRemoved = removeMentions(in: text) + var count = mentionsRemoved.count + for match in linkDetector.matches(in: mentionsRemoved, options: [], range: NSRange(location: 0, length: mentionsRemoved.utf16.count)) { + count -= match.range.length + count += 23 // Mastodon link length + } + return count + } + + private static func removeMentions(in text: String) -> String { + var mut = text + for match in mention.matches(in: mut, options: [], range: NSRange(location: 0, length: mut.utf16.count)).reversed() { + let replacement = mut[Range(match.range(at: 1), in: mut)!] + mut.replaceSubrange(Range(match.range, in: mut)!, with: replacement) + } + return mut + } + +} diff --git a/PachydermTests/CharacterCounterTests.swift b/PachydermTests/CharacterCounterTests.swift new file mode 100644 index 00000000..65220e96 --- /dev/null +++ b/PachydermTests/CharacterCounterTests.swift @@ -0,0 +1,43 @@ +// +// CharacterCounterTests.swift +// PachydermTests +// +// Created by Shadowfacts on 9/29/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import XCTest +@testable import Pachyderm + +class CharacterCounterTests: XCTestCase { + + override func setUp() { + } + + override func tearDown() { + } + + func testCountPlainText() { + XCTAssertEqual(CharacterCounter.count(text: "This is an example message"), 26) + XCTAssertEqual(CharacterCounter.count(text: "This is an example message with an Emoji: 😄"), 43) + XCTAssertEqual(CharacterCounter.count(text: "😄😄😄😄😄😄😄"), 7) + } + + func testCountLinks() { + XCTAssertEqual(CharacterCounter.count(text: "This is an example with a link: https://example.com"), 55) + XCTAssertEqual(CharacterCounter.count(text: "This is an example with a link 😄: https://example.com"), 57) + XCTAssertEqual(CharacterCounter.count(text: "😄😄😄😄😄😄😄: https://example.com"), 32) + XCTAssertEqual(CharacterCounter.count(text: "This is an example with a link: https://a.much.longer.example.com/link?foo=bar#baz"), 55) + } + + func testCountLocalMentions() { + XCTAssertEqual(CharacterCounter.count(text: "hello @example"), 14) + XCTAssertEqual(CharacterCounter.count(text: "@some_really_long_name"), 22) + } + + func testCountRemoteMentions() { + XCTAssertEqual(CharacterCounter.count(text: "hello @example@some.remote.social"), 14) + XCTAssertEqual(CharacterCounter.count(text: "hello @some_really_long_name@some-long.remote-instance.social"), 28) + } + +} diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index ff3b7153..3ebec944 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -109,6 +109,8 @@ D6D4DDDA212518A200E1C4BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6D4DDD8212518A200E1C4BB /* LaunchScreen.storyboard */; }; D6D4DDE5212518A200E1C4BB /* TuskerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDE4212518A200E1C4BB /* TuskerTests.swift */; }; D6D4DDF0212518A200E1C4BB /* TuskerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */; }; + D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26221603F8B006A8599 /* CharacterCounter.swift */; }; + D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E6F26421604242006A8599 /* CharacterCounterTests.swift */; }; D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */; }; D6F953EE21251A0700CF0F2B /* Timeline.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6F953ED21251A0700CF0F2B /* Timeline.storyboard */; }; D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6F953EF21251A2900CF0F2B /* MastodonController.swift */; }; @@ -276,6 +278,8 @@ D6D4DDEB212518A200E1C4BB /* TuskerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TuskerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D6D4DDEF212518A200E1C4BB /* TuskerUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerUITests.swift; sourceTree = ""; }; D6D4DDF1212518A200E1C4BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D6E6F26221603F8B006A8599 /* CharacterCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounter.swift; sourceTree = ""; }; + D6E6F26421604242006A8599 /* CharacterCounterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CharacterCounterTests.swift; sourceTree = ""; }; D6F953EB212519E700CF0F2B /* TimelineTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewController.swift; sourceTree = ""; }; D6F953ED21251A0700CF0F2B /* Timeline.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Timeline.storyboard; sourceTree = ""; }; D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = ""; }; @@ -330,6 +334,7 @@ D61099AE2144B0CC00432DC2 /* Info.plist */, D61099C82144B13C00432DC2 /* Client.swift */, D6109A0A2145953C00432DC2 /* ClientModel.swift */, + D6E6F26221603F8B006A8599 /* CharacterCounter.swift */, D61099D72144B74500432DC2 /* Extensions */, D61099CC2144B2C300432DC2 /* Request */, D61099DA2144BDB600432DC2 /* Response */, @@ -342,6 +347,7 @@ isa = PBXGroup; children = ( D61099BA2144B0CC00432DC2 /* PachydermTests.swift */, + D6E6F26421604242006A8599 /* CharacterCounterTests.swift */, D61099BC2144B0CC00432DC2 /* Info.plist */, ); path = PachydermTests; @@ -911,6 +917,7 @@ D6109A11214607D500432DC2 /* Timeline.swift in Sources */, D61099E7214561FF00432DC2 /* Attachment.swift in Sources */, D61099D02144B2D700432DC2 /* Method.swift in Sources */, + D6E6F26321603F8B006A8599 /* CharacterCounter.swift in Sources */, D61099FB214569F600432DC2 /* Report.swift in Sources */, D61099F92145698900432DC2 /* Relationship.swift in Sources */, D61099E12144C1DC00432DC2 /* Account.swift in Sources */, @@ -939,6 +946,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D6E6F26521604242006A8599 /* CharacterCounterTests.swift in Sources */, D61099BB2144B0CC00432DC2 /* PachydermTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0;