3
\$\begingroup\$

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?

asked Apr 11, 2017 at 11:33
\$\endgroup\$
1
  • \$\begingroup\$ it seems you should use regex \$\endgroup\$ Commented Apr 11, 2017 at 11:13

3 Answers 3

2
\$\begingroup\$

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)
jeh
1351 silver badge5 bronze badges
answered Apr 19, 2017 at 1:00
\$\endgroup\$
3
\$\begingroup\$

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"
answered Apr 11, 2017 at 11:46
\$\endgroup\$
3
\$\begingroup\$

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.
answered Apr 11, 2017 at 12:15
\$\endgroup\$
1
  • 1
    \$\begingroup\$ very good solutions. most i like the recursive solution because it becomes simple and self explaining code \$\endgroup\$ Commented Apr 11, 2017 at 23:57

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.