In Swift I have a string where a dash needs to be inserted after every 3 characters, if there are 4 characters or 2 characters at the end of the string they should be shown as "-xx-xx" or "-xx" respectively.
E.g "203940399345" to "203-940-399-345"
"2039403993454" to "203-940-399-34-54"
"20394039934546" to "203-940-399-345-46"
"2039403993454699409399" to "203-940-399-345-469-940-93-99"
I have the following solution:
func format(_ unformatted:String) -> String {
var formatted = ""
let count = unformatted.characters.count
unformatted.characters.enumerated().forEach {
if 0ドル.offset % 3 == 0 && 0ドル.offset != 0 && 0ドル.offset != count - 1 || 0ドル.offset == count - 2 && count % 3 != 0 {
formatted += "-" + String(0ドル.element)
return
}
formatted += String(0ドル.element)
return
}
return formatted
}
It will add a dash before each third char unless it is the first or last char or it is two from the end but only if the char count is divisible by 3 then it adds a char to the formatted string as normal.
Is there a more optimal way to achieve this formatting? What approach should I be taking with this kind of problem?
-
\$\begingroup\$ it seems you should use regex \$\endgroup\$Vyacheslav– Vyacheslav2017年04月11日 11:13:49 +00:00Commented Apr 11, 2017 at 11:13
3 Answers 3
The recursive approach is likely the most maintainable and flexible for this. It can be made fully configurable and can become an extension of the String type.
For example:
extension String
{
func group(by groupSize:Int=3, separator:String="-") -> String
{
if characters.count <= groupSize { return self }
let splitSize = min(max(2,characters.count-2) , groupSize)
let splitIndex = index(startIndex, offsetBy:splitSize)
return substring(to:splitIndex)
+ separator
+ substring(from:splitIndex).group(by:groupSize, separator:separator)
}
}
string.group(by:3)
Your code is working fine. I don't think you can optimize your code but you can use reduce and extend String as follow:
extension String {
var customFormatted: String {
let count = characters.count
return characters.enumerated().reduce("") { 1ドル.offset % 3 == 0 && 1ドル.offset != 0 && 1ドル.offset != count-1 || 1ドル.offset == count-2 && count % 3 != 0 ? 0ドル + "-\(1ドル.element)" : 0ドル + "\(1ドル.element)" }
}
}
Another option you have is to use map and join the resulting array:
extension String {
var customFormatted: String {
let count = characters.count
return characters.enumerated().map { 0ドル.offset % 3 == 0 && 0ドル.offset != 0 && 0ドル.offset != count-1 || 0ドル.offset == count-2 && count % 3 != 0 ? "-\(0ドル.element)" : "\(0ドル.element)" }.joined()
}
}
"2039403993454".customFormatted // "203-940-399-34-54"
There is one small bug in your code, a two-character string is formatted with an initial dash:
print(format("12")) // -12
There are various ways to fix that, e.g. by replacing the condition
0ドル.offset == count - 2 && count % 3 != 0
with
0ドル.offset == count - 2 && 0ドル.offset % 3 == 2
Now some suggestions to simplify the code and make it more readable. Instead of
unformatted.characters.enumerated().forEach {
// ... use `0ドル.offset` and `0ドル.element` ...
}
I would use for ... in
, so that we can give the loop variables names:
for (offset, char) in unformatted.characters.enumerated() {
// ... use `offset` and `char` ...
}
There is no need to convert 0ドル.element
to a String
because you can
append a character directly:
formatted.append(0ドル.element)
There is also no need to call return
at the end of the iteration block.
The condition
if 0ドル.offset % 3 == 0 && 0ドル.offset != 0 && 0ドル.offset != count - 1 || 0ドル.offset == count - 2 && 0ドル.offset % 2 == 2
is quite complex, I would split it into two conditions with if
/else if
.
Putting it all together, the function would then look like this:
func format(_ unformatted: String) -> String {
var formatted = ""
let count = unformatted.characters.count
for (offset, char) in unformatted.characters.enumerated() {
if offset > 0 && offset % 3 == 0 && offset != count - 1 {
formatted.append("-")
} else if offset % 3 == 2 && offset == count - 2 {
formatted.append("-")
}
formatted.append(char)
}
return formatted
}
The condition whether to insert the separator at an offset is quite complex and it is easy to make an error. A completely different approach would be a recursive implementation, which is (almost) self-explaining:
func format(_ str: String) -> String {
switch str.characters.count {
case 0...3: // No separators for strings up to length 3.
return str
case 4: // "abcd" -> "ab-cd"
let idx = str.index(str.startIndex, offsetBy: 2)
return str.substring(to: idx) + "-" + str.substring(from: idx)
default: // At least 5 characters. Separate the first three and recurse:
let idx = str.index(str.startIndex, offsetBy: 3)
return str.substring(to: idx) + "-" + format(str.substring(from: idx))
}
}
More suggestions:
- Choose a different name instead of
format()
which indicates the purpose of the formatting. - Make the separator character a parameter of the function.
-
1\$\begingroup\$ very good solutions. most i like the recursive solution because it becomes simple and self explaining code \$\endgroup\$muescha– muescha2017年04月11日 23:57:27 +00:00Commented Apr 11, 2017 at 23:57