Forward declare `let` to be initialized later

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?

How should I do this?

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.)

So this is what I end up:

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)
}
1 Like

Yep, that’s what I was thinking. You may also want to check if a string has two of the same numberPart, etc

What do you mean? My AttributedString is created using percent format:

0.49754.formatted(.percent.attributed)

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.

Can I do better?

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.

How is this possible?

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.

My code as of now:

    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
    }

1 Like