func breakApart(_ s: AttributedString) -> (integer: String, decimalSeparator: String, fraction: String, percent: String) {
let integerPart: String
let decimalSeparatorPart: String
let fractionPart: String
let percentPart: String
s.runs.forEach { run in
if let numberRun = run.numberPart {
switch numberRun {
case .integer:
integerPart = s[run.range].description // Cannot assign to value: 'integerPart' is a 'let' constant
case .fraction:
fractionPart = s[run.range].description // Cannot assign to value: 'fractionPart' is a 'let' constant
}
}
if let symbolRun = run.numberSymbol {
switch symbolRun {
case .decimalSeparator:
fractionPart = s[run.range].description // Cannot assign to value: 'fractionPart' is a 'let' constant
case .percent:
percentPart = s[run.range].description // Cannot assign to value: 'percentPart' is a 'let' constant
case .groupingSeparator:
break
case .sign:
break
case .currency:
break
@unknown default:
break
}
}
}
return (integerPart, decimalSeparatorPart, fractionPart, percentPart)
}
So the errors are because the compiler don't know if all the let's are assigned?
Use var, and possibly Optional. I know it stinks, but that’s what you do when you know more than the compiler does. (In this case you know that each value will be assigned exactly once by the end of the loop. Or do you? Maybe there are some edge cases you need to handle.)
func breakApart(_ s: AttributedString) -> (integer: String, decimalSeparator: String, fraction: String, percent: String) {
var integerPart: String?
var decimalSeparatorPart: String?
var fractionPart: String?
var percentPart: String?
s.runs.forEach { run in
if let numberRun = run.numberPart {
switch numberRun {
case .integer:
integerPart = String(s[run.range].characters) // is this the right way?
case .fraction:
fractionPart = String(s[run.range].characters) // is this the right way?
}
}
if let symbolRun = run.numberSymbol {
switch symbolRun {
case .decimalSeparator:
decimalSeparatorPart = String(s[run.range].characters) // is this the right way?
case .percent:
percentPart = String(s[run.range].characters) // is this the right way?
case .groupingSeparator:
break
case .sign:
break
case .currency:
break
@unknown default:
break
}
}
}
guard let integerPart, let decimalSeparatorPart, let fractionPart, let percentPart else {
fatalError("This AttributedString is not in percent format")
}
return (integerPart, decimalSeparatorPart, fractionPart, percentPart)
}
If you control the input string then it’s less important, sure. But that’s now an unchecked precondition of your function, and it’s worth acknowledging that. :-)
There is really no way to be wrong because the user input is a Double and I just formatted it using .percent.attributed and get this AttributedString. So no way to get some wrong formatted AttributedString because it's the FormatStyle that produce this.
Anyway, just to catch any possible error, the guard at the tail before return is a runtime check.
That’s guarding against a missing section, but it doesn’t guard against a duplicate section like “1.2.3%” (assuming that was attributed appropriately). You’d have to check for nil before assigning to your result variables to catch that.
func foo(_ value: Double) {
var s = value.formatted(.percent.attributed) // <== it's from here
// so now how is that "duplicate segment" happen?
// I then go on to "parse" out the 4 runs that's now in this AttributtedString
}
If I understand, you want me to check for nil before assigning? and if it's not nil it's an error condition?
Again, if you control the input it shouldn’t happen. But you don’t quite control the input here; you’re using Foundation, which could change across OS versions. (Unless they document that the output will have precisely one of each attribute, in which case it would be rather unfair of them to change it!)
Anyway, you’re probably fine. I just wanted to cover all bases in case someone else has a similar problem but doesn’t have as much control over the input string.
static func applyingFormatStyle(_ value: Double) -> AttributedString {
var s = value.formatted(.percent.precision(.fractionLength(1...2)).attributed)
let font = Font.system(size: 250, weight: .bold, design: .rounded)
let fontSmall = Font.system(size: 150, weight: .bold, design: .rounded)
var integerRange: Range<AttributedString.Index>?
var decimalSeparatorRange: Range<AttributedString.Index>?
var fractionRange: Range<AttributedString.Index>?
var percentRange: Range<AttributedString.Index>?
s.runs.forEach { run in
if let numberRun = run.numberPart {
switch numberRun {
case .integer:
assert(integerRange == nil, "Seen integer part already form input \(value)")
integerRange = run.range
case .fraction:
assert(fractionRange == nil, "Seen fraction part already form input \(value)")
fractionRange = run.range
@unknown default:
break
}
}
if let symbolRun = run.numberSymbol {
switch symbolRun {
case .decimalSeparator:
assert(decimalSeparatorRange == nil, "Seen decimalSeparator part already form input \(value)")
decimalSeparatorRange = run.range
case .percent:
assert(percentRange == nil, "Seen percent part already form input \(value)")
percentRange = run.range
case .groupingSeparator:
break
case .sign:
break
case .currency:
break
@unknown default:
break
}
}
}
guard let integerRange, let decimalSeparatorRange, let fractionRange, let percentRange else {
fatalError("This AttributedString is not in percent format")
}
s[integerRange].font = font
s[decimalSeparatorRange].font = fontSmall
s[fractionRange].font = fontSmall
s[percentRange].font = font
return s
}