HiveBrain v1.2.0
Get Started
← Back to all entries
snippetswiftMinor

Swift format string with separator every n characters where n changes

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
swiftformatwithwhereeveryseparatorchangescharactersstring

Problem

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?

Solution

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.

Code Snippets

print(format("12")) // -12
$0.offset == count - 2 && count % 3 != 0
$0.offset == count - 2 && $0.offset % 3 == 2
unformatted.characters.enumerated().forEach {
     // ... use `$0.offset` and `$0.element` ...
}
for (offset, char) in unformatted.characters.enumerated() {
     // ... use `offset` and `char` ...
}

Context

StackExchange Code Review Q#160415, answer score: 3

Revisions (0)

No revisions yet.