I've created the following data model:
import Foundation
struct Effort {
let amount: Int
let unit: String
var text: String {
"\(amount) \(unit)"
}
var attributedText: String {
String(
AttributedString(localized: "^[\(amount) \(unit)](inflect: true)").characters
)
}
init(amount: Int, unit: String) {
self.amount = amount
self.unit = unit
}
}
If I create the value with the singular unit
name, the automatic grammar agreement
(/) works perfectly:
Effort(amount: 4, unit: "day").attributedString
"4 days"
Effort(amount: 1, unit: "day").attributedString
"1 day"
However, if I create this struct with already plural string, the automatic grammar agreement won't actually turn it back into singular:
Effort(amount: 1, unit: "days").attributedString
"1 days"
Is there any way to make it work both ways? I'm open to refactoring this code, even if it means it won't be so "elegant", i.e. if I have to write the inflection rule manually.
Motivation: the server already returns values in a plural form, also it would be good to make the client completely agnostic to the server response.
One idea that I'm considering looks like this:
AttributedString(localized: "^[\(amount) \(String(unit.dropLast()))](inflect: true)")
In essence, we're just dropping the last character from the unit
, but this obviously is not so robust as if the system handled it on it's own.
I've created the following data model:
import Foundation
struct Effort {
let amount: Int
let unit: String
var text: String {
"\(amount) \(unit)"
}
var attributedText: String {
String(
AttributedString(localized: "^[\(amount) \(unit)](inflect: true)").characters
)
}
init(amount: Int, unit: String) {
self.amount = amount
self.unit = unit
}
}
If I create the value with the singular unit
name, the automatic grammar agreement
(https://www.swiftjectivec/morphology-in-ios-with-automatic-grammar-agreement/) works perfectly:
Effort(amount: 4, unit: "day").attributedString
"4 days"
Effort(amount: 1, unit: "day").attributedString
"1 day"
However, if I create this struct with already plural string, the automatic grammar agreement won't actually turn it back into singular:
Effort(amount: 1, unit: "days").attributedString
"1 days"
Is there any way to make it work both ways? I'm open to refactoring this code, even if it means it won't be so "elegant", i.e. if I have to write the inflection rule manually.
Motivation: the server already returns values in a plural form, also it would be good to make the client completely agnostic to the server response.
One idea that I'm considering looks like this:
AttributedString(localized: "^[\(amount) \(String(unit.dropLast()))](inflect: true)")
In essence, we're just dropping the last character from the unit
, but this obviously is not so robust as if the system handled it on it's own.
2 Answers
Reset to default 3Consider lemmatising whatever you get from the server, so that it becomes the singular form, before you pass the string to Effort.init
.
Here is an example:
import NaturalLanguage
func lemmatiseNoun(_ input: String) -> String {
let tagger = NLTagger(tagSchemes: [.lemma])
let string = "the \(input)"
tagger.string = string
tagger.setLanguage(.english, range: string.startIndex..<string.endIndex)
let index = string.index(string.startIndex, offsetBy: 4)
let (tag, _) = tagger.tag(at: index, unit: .word, scheme: .lemma)
return tag?.rawValue ?? input
}
lemmatiseNoun("days") // "day"
Note that I am prefixing the word with the article "the", so that the tagger treats it as a noun. This is to avoid cases such as "leaves", where the tagger will lemmatise it as "leave" instead of "leaf" if it is just the word on its own.
As always with natural languages, there are fundamentally ambiguous cases. For example, consider "people". It is both the plural form of "person" (a human being), and also the singular form of "peoples" (the members of a particular nation, community, or ethnic group).
I won't compete with Sweeper's NLTagger
but at least it is a little bit better than just dropping the last letter:
private func singular(_ word: String) -> String {
if word.hasSuffix("ies") {
String(word.dropLast(3)) + "y"
} else if word.hasSuffix("ves") {
String(word.dropLast(3)) + "f"
} else if word.hasSuffix("oes") {
String(word.dropLast(2))
} else if word.hasSuffix("s") {
String(word.dropLast())
} else {
word
}
}
You can also include irregular plural nouns if you need:
private func singular(_ word: String) -> String {
let irregularPlurals: [String: String] = [
"children": "child",
"men": "man",
// ...
]
if let singular = irregularPlurals[word.lowercased()] {
return singular
}
if word.hasSuffix("ies") {
//...
NLTagger
with thelemma
tag scheme to convert the plural form to the singular form before passing it toEffort
? – Sweeper Commented Mar 17 at 17:21unit
to a an enum instead of String? Have the enum map to the appropriate String value that matches the needed key. The enum also prevents a coder from typing an invalid/unsupported unit. – HangarRash Commented Mar 17 at 17:21