From eb041435585cd558ddd7b3c5f2ff4ac650c03d10 Mon Sep 17 00:00:00 2001 From: Balazs Perlaki-Horvath Date: Sun, 4 Aug 2024 15:31:43 +0200 Subject: [PATCH] Fix multilanguage ZIM listing and count, add unit tests --- Model/Entities/Entities.swift | 3 +- Model/LanguageCollector.swift | 44 +++++++++++++++ Tests/LanguageCollectorTest.swift | 80 +++++++++++++++++++++++++++ Views/Settings/LanguageSelector.swift | 14 +++-- 4 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 Model/LanguageCollector.swift create mode 100644 Tests/LanguageCollectorTest.swift diff --git a/Model/Entities/Entities.swift b/Model/Entities/Entities.swift index 30325cce..2aed88af 100644 --- a/Model/Entities/Entities.swift +++ b/Model/Entities/Entities.swift @@ -67,7 +67,8 @@ struct Language: Identifiable, Comparable { let count: Int init?(code: String, count: Int) { - guard let name = Locale.current.localizedString(forIdentifier: code) else { return nil } + guard let name = Locale.current.localizedString(forIdentifier: code), + count > 0 else { return nil } self.code = code self.name = name self.count = count diff --git a/Model/LanguageCollector.swift b/Model/LanguageCollector.swift new file mode 100644 index 00000000..29c8d689 --- /dev/null +++ b/Model/LanguageCollector.swift @@ -0,0 +1,44 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import Foundation + +/// Helper to collect all language codes with counts, +/// some of them are coma separated entries in the DB, such as "eng,spa,por" +final class LanguageCollector { + + private var items: [String: Int] = [:] + + func addLanguages(codes: String, count: Int) { + Set(codes.split(separator: ",")).forEach { code in + addLanguage(code: String(code), count: count) + } + } + + func languages() -> [Language] { + items.compactMap { (code: String, count: Int) -> Language? in + Language(code: code, count: count) + }.sorted() + } + + private func addLanguage(code: String, count: Int) { + if let previousCount = items[code] { + items[code] = previousCount + count + } else { + items[code] = count + } + } + +} diff --git a/Tests/LanguageCollectorTest.swift b/Tests/LanguageCollectorTest.swift new file mode 100644 index 00000000..231dfb1d --- /dev/null +++ b/Tests/LanguageCollectorTest.swift @@ -0,0 +1,80 @@ +// This file is part of Kiwix for iOS & macOS. +// +// Kiwix is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 3 of the License, or +// any later version. +// +// Kiwix is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Kiwix; If not, see https://www.gnu.org/licenses/. + +import XCTest +@testable import Kiwix + +final class LanguageCollectorTest: XCTestCase { + + func testEmpty() { + let collector = LanguageCollector() + XCTAssertTrue(collector.languages().isEmpty) + } + + func testInvalidEntriesIgnored() { + let collector = LanguageCollector() + collector.addLanguages(codes: "invalid", count: 1) + XCTAssertTrue(collector.languages().isEmpty) + collector.addLanguages(codes: "more,invalid,entries", count: 2) + XCTAssertTrue(collector.languages().isEmpty) + collector.addLanguages(codes: "i_am,invalid,fra", count: 1) + XCTAssertEqual(collector.languages().count, 1) + XCTAssertEqual(collector.languages().first!.name, "French") + } + + func testZeroAndNegativeCountsIgnored() { + let collector = LanguageCollector() + collector.addLanguages(codes: "eng", count: 0) + XCTAssertTrue(collector.languages().isEmpty) + collector.addLanguages(codes: "eng,fra", count: -1) + XCTAssertTrue(collector.languages().isEmpty) + } + + func testAddingSingleLanguages() { + let collector = LanguageCollector() + collector.addLanguages(codes: "eng", count: 1) + XCTAssertEqual(collector.languages().count, 1) + XCTAssertEqual(collector.languages().first!.name, "English") + XCTAssertEqual(collector.languages().first!.code, "eng") + XCTAssertEqual(collector.languages().first!.count, 1) + } + + func testRepeatedLanguagesCodesAreIgnored() { + let collector = LanguageCollector() + collector.addLanguages(codes: "eng,eng", count: 1) + XCTAssertEqual(collector.languages().count, 1) + XCTAssertEqual(collector.languages().first!.count, 1) + } + + func testAddingMultipleLanguagesWithCountOne() { + let collector = LanguageCollector() + collector.addLanguages(codes: "fra,eng", count: 1) + XCTAssertEqual(collector.languages().count, 2) + XCTAssertEqual(collector.languages().map { $0.name }, ["English", "French"]) + } + + func testAddingMultiLanguagesWithVariousCounts() { + let collector = LanguageCollector() + collector.addLanguages(codes: "fra,eng", count: 1) + collector.addLanguages(codes: "spa,por,fra", count: 1) + XCTAssertEqual(collector.languages().count, 4) + XCTAssertEqual(collector.languages(), [ + Language(code: "eng", count: 1)!, + Language(code: "fra", count: 2)!, + Language(code: "por", count: 1)!, + Language(code: "spa", count: 1)! + ]) + } +} diff --git a/Views/Settings/LanguageSelector.swift b/Views/Settings/LanguageSelector.swift index fc5a2a9e..18948d0b 100644 --- a/Views/Settings/LanguageSelector.swift +++ b/Views/Settings/LanguageSelector.swift @@ -170,13 +170,15 @@ class Languages { continuation.resume(returning: []) return } - let language: [Language] = results.compactMap { result in - guard let result = result as? NSDictionary, - let languageCode = result["languageCode"] as? String, - let count = result["count"] as? Int else { return nil } - return Language(code: languageCode, count: count) + let collector = LanguageCollector() + for result in results { + if let result = result as? NSDictionary, + let languageCodes = result["languageCode"] as? String, + let count = result["count"] as? Int { + collector.addLanguages(codes: languageCodes, count: count) + } } - continuation.resume(returning: language) + continuation.resume(returning: collector.languages()) } } return languages