Loosely inspired by Ruby's String.count-method I wrote a similar Swift-method myself as an extension to the String-class. Then I had the idea to use a second parameter with which you can control if the search shall be done case-sensitive or case-insensitive.
Here's my code:
extension String {
func countOccurrences(of char: Character, doesIgnoreCase: Bool = false) -> Int {
var str = self
var comp = String(char)
if doesIgnoreCase == true {
str = str.lowercased()
comp = comp.lowercased()
}
return str.reduce(0) { acc, curr in
if String(curr) == comp {
return acc + 1
}
return acc
}
}
}
let str = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
print(str.self.countOccurrences(of: "l", doesIgnoreCase: false)) // 2
print(str.self.countOccurrences(of: "l", doesIgnoreCase: true)) // 3
What I consider a bit annoying is the whole type-casting between String and Char. Is there a way around it?
What's your opinion about my idea with the 'doesIgnoreCase'-parameter? Is there a better approach for supporting both possibilities?
Any other comments and answers concerning my implementation appreciated as well.
2 Answers 2
extension String {
func countOccurrences(of char: Character, ignoreCase: Bool = false) -> Int {
let comparisonCharacter = ignoreCase ? Character(char.lowercased()) : char
let searchString = ignoreCase ? self.lowercased() : self
return searchString.filter { 0ドル == comparisonCharacter }.count
}
}
let str = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit."
print(str.countOccurrences(of: "l", ignoreCase: false)) // 2
print(str.countOccurrences(of: "l", ignoreCase: true)) // 3
With this enhanced version to avoid type-casting between Character
and String
, you can compare characters directly by working with Character
types in both str
and comp
.
I've also used the filter
and count
final operations to collect only the matching characters and then get its amount.
p.s: for complex patterns I think you should use a NSRegularExpression
, it handles in a better way multiple characters and advanced search patterns into char sequences.
-
\$\begingroup\$ According to the doumentation,
char.lowercased()
may return a string of multple characters. In that caseCharacter(char.lowercased())
would abort with a runtime exception. – It seems that this does not actually happen with the current Unicode standard, but do you want to rely on that it does not happen in a future version? \$\endgroup\$Martin R– Martin R2024年11月03日 03:22:43 +00:00Commented Nov 3, 2024 at 3:22 -
\$\begingroup\$
filter
returns an intermediate array, that may be inefficient with large strings.count(where:)
might be a better alternative. \$\endgroup\$Martin R– Martin R2024年11月03日 03:24:22 +00:00Commented Nov 3, 2024 at 3:24 -
\$\begingroup\$ Yes I just trust they'll fix
Character(char.lowercased())
in the next versions. Regarding thefilter
operation you're right, it's an intermediate one, but for the case of handling large strings I've added a post scriptum in the end of my answer. \$\endgroup\$Adversing– Adversing2024年11月03日 03:29:53 +00:00Commented Nov 3, 2024 at 3:29
Naming
The Swift API Design Guidelines have a section about naming, with recommendations like
- Omit needless words.
- Prefer method and function names that make use sites form grammatical English phrases.
In this spirit I would replace doesIgnoreCase
by ignoringCase
:
func countOccurrences(of char: Character, ignoringCase: Bool = false) -> Int
An alternative would be
func countOccurrences(of char: Character, caseInsensitive: Bool = false) -> Int
resembling the names of existing comparison option and functions.
No need to compare boolean values
A test == true
or == false
is always redundant. In your code
if doesIgnoreCase == true { ... }
is shorter and more concisely expressed as
if doesIgnoreCase { ... }
Character to String conversions
The Character.lowerCased() method states:
Because case conversion can result in multiple characters, the result of lowercased() is a string.
Whether this can actually happen or not was recently discussed in the Swift forum:
where it is said that this does not happen with the current Unicode standard, but that it is possible that this could be the case at some point in the future.
It definitely happens with conversions to upper case, as demonstrated in this example:
let c: Character = "ß" // The "German eszett"
let s = c.uppercased()
print(s, s.count)
// SS 2
I would therefore not forcibly convert char.lowercased()
to a Character
and risk a runtime error.
The number of conversions can still be reduced a bit if you convert each character of the string to lower case separately, i.e.
if curr.lowercased() == comp
for the case insensitive comparison.
Miscellaneous
Use the conditional operator ?:
and omit needless return
keywords, i.e.
if String(curr) == comp {
return acc + 1
}
return acc
in the closure can be shortened to
String(curr) == comp ? acc + 1 : acc
Use existing sequence methods
There is already count(where:)
which returns the number of elements in the sequence that satisfy the given predicate.
Updated code
Putting it all together, this is how I would write the method:
extension String {
func countOccurrences(of char: Character, ignoringCase: Bool = false) -> Int {
if ignoringCase {
let lowercasedChar = char.lowercased()
return count(where: { 0ドル.lowercased() == lowercasedChar })
} else {
return count(where: { 0ドル == char })
}
}
}
Explore related questions
See similar questions with these tags.