From ef6478dcc67b70aec8b583244a74f8041219be0a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 18 Mar 2024 01:53:28 +0000 Subject: [PATCH] [Library] Word filtering in Foundation library (#6) This PR contains the work done to Implemented the `FilterWordsUseCase` use case in the Foundation library, which will be used to filter the content of the reviews. Reviewed-on: https://repo.rock-n-code.com/rock-n-code/app-reviews/pulls/6 Co-authored-by: Javier Cicchelli Co-committed-by: Javier Cicchelli --- .../Use Cases/FilterWordsUseCase.swift | 120 +++++++++++++++++ .../Use Cases/FilterWordsUseCaseTests.swift | 125 ++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 Libraries/Foundation/Kit/Sources/Use Cases/FilterWordsUseCase.swift create mode 100644 Libraries/Foundation/Test/Tests/Use Cases/FilterWordsUseCaseTests.swift diff --git a/Libraries/Foundation/Kit/Sources/Use Cases/FilterWordsUseCase.swift b/Libraries/Foundation/Kit/Sources/Use Cases/FilterWordsUseCase.swift new file mode 100644 index 0000000..b158f3f --- /dev/null +++ b/Libraries/Foundation/Kit/Sources/Use Cases/FilterWordsUseCase.swift @@ -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,}" + } +} diff --git a/Libraries/Foundation/Test/Tests/Use Cases/FilterWordsUseCaseTests.swift b/Libraries/Foundation/Test/Tests/Use Cases/FilterWordsUseCaseTests.swift new file mode 100644 index 0000000..8cfeae2 --- /dev/null +++ b/Libraries/Foundation/Test/Tests/Use Cases/FilterWordsUseCaseTests.swift @@ -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, []) + } + +}