[Library] Word filtering in Foundation library #6

Merged
javier merged 1 commits from library/foundation/filter into main 2024-03-18 01:53:28 +00:00
2 changed files with 245 additions and 0 deletions

View File

@ -0,0 +1,120 @@
//
// FilterWordsUseCase.swift
// ReviewsParserKit
//
// Created by Javier Cicchelli on 17/03/2024.
// Copyright © 2024 Röck+Cöde VoF. All rights reserved.
//
import Foundation
public struct FilterWordsUseCase {
// MARK: Type aliases
public typealias Input = String
public typealias Output = [Tuple]
// MARK: Initialisers
public init() {}
// MARK: Functions
public func callAsFunction(_ input: Input) throws -> Output {
let regularExpression = try NSRegularExpression(
pattern: .Pattern.wordsLongerThan4Characters,
options: .caseInsensitive
)
let matches = regularExpression.matches(
in: input,
options: [],
range: .init(input.startIndex..., in: input)
)
guard !matches.isEmpty else { return [] }
let wordsAll = matches.compactMap {
Range($0.range, in: input).map { String(input[$0]) }
}
let wordsUnique = wordsAll
.reduce(into: [String]()) { partialResult, word in
guard partialResult
.filter({ compareWords(word, $0) })
.isEmpty
else { return }
partialResult.append(
word.folding(
options: [
.caseInsensitive,
.diacriticInsensitive
],
locale: .current
)
.capitalized
)
}
return Dictionary(grouping: wordsUnique) { word in
wordsAll
.filter { compareWords(word, $0) }
.count
}
.flatMap { (key, values) -> Output in
values.map {
.init(word: $0, count: key)
}
}
.sorted {
guard $0.count != $1.count else {
return $0.word < $1.word
}
return $0.count > $1.count
}
}
}
// MARK: - Helpers
private extension FilterWordsUseCase {
// MARK: Functions
func compareWords(
_ lword: String,
_ rword: String
) -> Bool {
lword.compare(rword, options: [
.caseInsensitive,
.diacriticInsensitive
]) == .orderedSame
}
}
// MARK: - Structs
extension FilterWordsUseCase {
public struct Tuple: Equatable {
// MARK: Constants
let word: String
let count: Int
// MARK: Initialisers
public init(
word: String,
count: Int
) {
self.word = word
self.count = count
}
}
}
// MARK: - String+Constants
private extension String {
enum Pattern {
static let wordsLongerThan4Characters = "\\w{4,}"
}
}

View File

@ -0,0 +1,125 @@
//
// FilterWordsUseCaseTests.swift
// ReviewsFoundationTest
//
// Created by Javier Cicchelli on 18/03/2024.
// Copyright © 2024 Röck+Cöde VoF. All rights reserved.
//
import ReviewsFoundationKit
import XCTest
final class FilterWordsUseCaseTests: XCTestCase {
// MARK: Properties
private var input: FilterWordsUseCase.Input!
private var output: FilterWordsUseCase.Output!
private var sut: FilterWordsUseCase!
// MARK: Setup
override func setUp() async throws {
sut = .init()
}
// MARK: Function tests
func testCallAsFunction_withInput_hasSomeLongWords() throws {
// GIVEN
input = "one two three four five six seven eight nine ten"
// WHEN
output = try sut(input)
// THEN
XCTAssertFalse(output.isEmpty)
XCTAssertEqual(output, [
.init(word: "Eight", count: 1),
.init(word: "Five", count: 1),
.init(word: "Four", count: 1),
.init(word: "Nine", count: 1),
.init(word: "Seven", count: 1),
.init(word: "Three", count: 1),
])
}
func testCallAsFunction_withInput_hasSomeRepeatedLongWords() throws {
// GIVEN
input = "one two three three four five five six seven eight nine nine ten"
// WHEN
output = try sut(input)
// THEN
XCTAssertFalse(output.isEmpty)
XCTAssertEqual(output, [
.init(word: "Five", count: 2),
.init(word: "Nine", count: 2),
.init(word: "Three", count: 2),
.init(word: "Eight", count: 1),
.init(word: "Four", count: 1),
.init(word: "Seven", count: 1),
])
}
func testCallAsFunction_withInput_hasSomeRepeatedCaseSensitiveLongWords() throws {
// GIVEN
input = "one two three Three four Five five six seven eight nine nine ten"
// WHEN
output = try sut(input)
// THEN
XCTAssertFalse(output.isEmpty)
XCTAssertEqual(output, [
.init(word: "Five", count: 2),
.init(word: "Nine", count: 2),
.init(word: "Three", count: 2),
.init(word: "Eight", count: 1),
.init(word: "Four", count: 1),
.init(word: "Seven", count: 1),
])
}
func testCallAsFunction_withInput_hasSomeRepeatedDiacriticSensitiveLongWords() throws {
// GIVEN
input = "one two thrèé Three four Fíve fïve six Šëvêń seven eight niñe nine ten"
// WHEN
output = try sut(input)
// THEN
XCTAssertFalse(output.isEmpty)
XCTAssertEqual(output, [
.init(word: "Five", count: 2),
.init(word: "Nine", count: 2),
.init(word: "Seven", count: 2),
.init(word: "Three", count: 2),
.init(word: "Eight", count: 1),
.init(word: "Four", count: 1),
])
}
func testCallAsFunction_withInput_hasOnlyShortWords() throws {
// GIVEN
input = "no one is a two"
// WHEN
output = try sut(input)
// THEN
XCTAssertTrue(output.isEmpty)
XCTAssertEqual(output, [])
}
func testCallAsFunction_withEmptyInput() throws {
// GIVEN
input = .empty
// WHEN
output = try sut(input)
// THEN
XCTAssertTrue(output.isEmpty)
XCTAssertEqual(output, [])
}
}