File: ByteCountFormatStyle.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (335 lines) | stat: -rw-r--r-- 12,931 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#if canImport(FoundationEssentials)
import FoundationEssentials
#endif

internal import _FoundationICU

@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public struct ByteCountFormatStyle: FormatStyle, Sendable {
    public var style: Style { get { attributed.style } set { attributed.style = newValue} }
    public var allowedUnits: Units { get { attributed.allowedUnits } set { attributed.allowedUnits = newValue} }
    public var spellsOutZero: Bool { get { attributed.spellsOutZero } set { attributed.spellsOutZero = newValue} }
    public var includesActualByteCount: Bool { get { attributed.includesActualByteCount } set { attributed.includesActualByteCount = newValue} }
    public var locale: Locale { get { attributed.locale } set { attributed.locale = newValue} }
    public var attributed: Attributed

    internal enum Unit: Int {
        case byte = 0
        case kilobyte
        case megabyte
        case gigabyte
        case terabyte
        case petabyte
        // 84270854: The ones below are still pending support by ICU 69.
        case exabyte
        case zettabyte
        case yottabyte

        var name: String {
            Self.unitNames[rawValue]
        }

        var decimalSize: Int64 {
            Self.decimalByteSizes[rawValue]
        }

        var binarySize: Int64 {
            Self.binaryByteSizes[rawValue]
        }

        static let unitNames = ["byte", "kilobyte", "megabyte", "gigabyte", "terabyte", "petabyte"]
        static let decimalByteSizes: [Int64] = [1, 1_000, 1_000_000, 1_000_000_000, 1_000_000_000_000, 1_000_000_000_000_000]
        static let binaryByteSizes: [Int64] = [1, 1024, 1048576, 1073741824, 1099511627776, 1125899906842624]
    }

    public func format(_ value: Int64) -> String {
        String(attributed.format(value).characters)
    }

    public func locale(_ locale: Locale) -> Self {
        var new = self
        new.locale = locale
        return new
    }

    public init(style: Style = .file, allowedUnits: Units = .all, spellsOutZero: Bool = true, includesActualByteCount: Bool = false, locale: Locale = .autoupdatingCurrent) {
        self.attributed = Attributed(style: style, allowedUnits: allowedUnits, spellsOutZero: spellsOutZero, includesActualByteCount: includesActualByteCount, locale: locale)
    }

    public enum Style: Int, Codable, Hashable, Sendable {
        case file = 0, memory, decimal, binary
    }

    public struct Units: OptionSet, Codable, Hashable, Sendable {
        public var rawValue: UInt

        public init(rawValue: UInt) {
            if rawValue == 0 {
                self = .all
            } else {
                self.rawValue = rawValue
            }
        }

        public static var bytes: Self { Self(rawValue: 1 << 0) }
        public static var kb: Self { Self(rawValue: 1 << 1) }
        public static var mb: Self { Self(rawValue: 1 << 2) }
        public static var gb: Self { Self(rawValue: 1 << 3) }
        public static var tb: Self { Self(rawValue: 1 << 4) }
        public static var pb: Self { Self(rawValue: 1 << 5) }
        public static var eb: Self { Self(rawValue: 1 << 6) }
        public static var zb: Self { Self(rawValue: 1 << 7) }
        public static var ybOrHigher: Self { Self(rawValue: 0x0FF << 8) }

        public static var all: Self { .init(rawValue: 0x0FFFF) }
        public static var `default`: Self { .all }

        fileprivate var smallestUnit: Unit {
            for idx in (Unit.byte.rawValue...Unit.petabyte.rawValue) {
                if self.contains(.init(rawValue: UInt(idx))) { return Unit(rawValue: idx)! }
            }
            // 84270854: Fall back to petabyte if the unit is larger than petabyte, which is the largest we currently support
            return .petabyte
        }
    }

    public struct Attributed: FormatStyle, Sendable {
        public var style: Style
        public var allowedUnits: Units
        public var spellsOutZero: Bool
        public var includesActualByteCount: Bool
        public var locale: Locale

        public func locale(_ locale: Locale) -> Self {
            var new = self
            new.locale = locale
            return new
        }

        // Max sizes to use for a given unit.
        // These sizes take into account the precision of each unit. e.g. 1023.95 MB should be formatted as 1 GB since MB only uses 1 fraction digit
        fileprivate static let maxDecimalSizes = [999, 999499, 999949999, 999994999999, 999994999999999, Int64.max]
        fileprivate static let maxBinarySizes = [1023, 1048063, 1073689395, 1099506259066, 1125894409284485, Int64.max]

        func useSpelloutZero(forLocale locale: Locale, unit: Unit) -> Bool {
            guard unit == .byte || unit == .kilobyte else { return false }

            guard let languageCode = locale.language.languageCode?._normalizedIdentifier else { return false }

            switch languageCode {
            case "ar", "da", "el", "en", "fr",  "hi", "hr", "id", "it", "ms", "pt", "ro", "th":
                return true
            default:
                break
            }

            guard unit == .byte else { return false }

            // These only uses spellout zero with byte but not with kilobyte
            switch languageCode {
            case "ca", "no":
                return true
            default:
                break
            }

            return false
        }
        
        func _format(_ formatterValue: ICUNumberFormatter.Value, doubleValue: Double) -> AttributedString {
            let unit: Unit = allowedUnits.contains(.kb) ? .kilobyte : .byte
            if spellsOutZero && doubleValue.isZero {
                let numberFormatter = ICUByteCountNumberFormatter.create(for: "measure-unit/digital-\(unit.name)\(unit == .byte ? " unit-width-full-name" : "")", locale: locale)
                guard var attributedFormat = numberFormatter?.attributedFormat(.integer(.zero), unit: unit) else {
                    // fallback to English if ICU formatting fails
                    return unit == .byte ? "Zero bytes" : "Zero kB"
                }

                guard useSpelloutZero(forLocale: locale, unit: unit) else {
                    return attributedFormat
                }

                let configuration = DescriptiveNumberFormatConfiguration.Collection(presentation: .cardinal, capitalizationContext: .beginningOfSentence)
                guard let spellOutFormatter = ICULegacyNumberFormatter.formatter(for: .descriptive(configuration), locale: locale) else {
                    return attributedFormat
                }

                guard let zeroFormatted = spellOutFormatter.format(Int64.zero) else {
                    return attributedFormat
                }

                var attributedZero = AttributedString(zeroFormatted)
                attributedZero.byteCount = .spelledOutValue
                for (value, range) in attributedFormat.runs[\.byteCount] where value == .value {
                    attributedFormat.replaceSubrange(range, with: attributedZero)
                }

                return attributedFormat
            }

            let decimal: Bool
            let maxSizes: [Int64]
            switch style {
            case .file, .decimal:
                decimal = true
                maxSizes = Self.maxDecimalSizes
            case .memory, .binary:
                decimal = false
                maxSizes = Self.maxBinarySizes
            }

            let absValue = abs(doubleValue)
            let bestUnit: Unit = {
                var bestUnit = allowedUnits.smallestUnit
                for (idx, size) in maxSizes.enumerated() {
                    guard allowedUnits.contains(.init(rawValue: 1 << idx)) else {
                        continue
                    }
                    bestUnit = Unit(rawValue: idx)!
                    if absValue < Double(size) {
                        break
                    }
                }

                return bestUnit
            }()

            let denominator = decimal ? bestUnit.decimalSize : bestUnit.binarySize
            let unitValue = doubleValue/Double(denominator)

            let precisionSkeleton: String
            switch bestUnit {
            case .byte, .kilobyte:
                precisionSkeleton = "." // 0 fraction digits
            case .megabyte:
                precisionSkeleton = ".#" // Up to one fraction digit
            default:
                precisionSkeleton = ".##" // Up to two fraction digits
            }

            let formatter = ICUByteCountNumberFormatter.create(for: "\(precisionSkeleton) measure-unit/digital-\(bestUnit.name) \(bestUnit == .byte ? "unit-width-full-name" : "")", locale: locale)

            var attributedString = formatter!.attributedFormat(.floatingPoint(unitValue), unit: bestUnit)

            if includesActualByteCount {
                let byteFormatter = ICUByteCountNumberFormatter.create(for: "measure-unit/digital-byte unit-width-full-name", locale: locale)

                let localizedParens = localizedParens(locale: locale)
                attributedString.append(AttributedString(localizedParens.0))

                var attributedBytes = byteFormatter!.attributedFormat(formatterValue, unit: .byte)
                for (value, range) in attributedBytes.runs[\.byteCount] where value == .value {
                    attributedBytes[range].byteCount = .actualByteCount
                }
                attributedString.append(attributedBytes)

                attributedString.append(AttributedString(localizedParens.1))
            }

            return attributedString
        }
        
        public func format(_ value: Int64) -> AttributedString {
            _format(.integer(value), doubleValue: Double(value))
        }
    }
}

@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
public extension FormatStyle where Self == ByteCountFormatStyle {
    static func byteCount(style: ByteCountFormatStyle.Style, allowedUnits: ByteCountFormatStyle.Units = .all, spellsOutZero: Bool = true, includesActualByteCount: Bool = false) -> Self {
        return ByteCountFormatStyle(style: style, allowedUnits: allowedUnits, spellsOutZero: spellsOutZero, includesActualByteCount: includesActualByteCount)
    }

}

private func localizedParens(locale: Locale) -> (String, String) {
    var status = U_ZERO_ERROR

    let ulocdata = locale.identifier.withCString {
        ulocdata_open($0, &status)
    }
    defer { ulocdata_close(ulocdata) }

    guard status.checkSuccessAndLogError("ulocdata_open failed.") else {
        return (" (", ")")
    }

    let exemplars = ulocdata_getExemplarSet(ulocdata, nil, 0, .punctuation, &status)
    defer { uset_close(exemplars) }

    guard status.checkSuccessAndLogError("ulocdata_getExemplarSet failed.") else {
        return (" (", ")")
    }
    
    let fullwidthLeftParenUTF32 = 0x0000FF08 as Int32
    let containsFullWidth = uset_contains(exemplars!, fullwidthLeftParenUTF32).boolValue

    if containsFullWidth {
        return ("(", ")")
    } else {
        return (" (", ")")
    }
}

extension AttributeScopes.FoundationAttributes.ByteCountAttribute.Component {
    internal init?(unumberFormatField: UNumberFormatFields, unit: ByteCountFormatStyle.Unit) {
        switch unumberFormatField {
        case .integer:
            self = .value
        case .fraction:
            self = .value
        case .decimalSeparator:
            self = .value
        case .groupingSeparator:
            self = .value
        case .sign:
            self = .value
        case .currencySymbol:
            return nil
        case .percentSymbol:
            return nil
        case .measureUnit:
            self = .unit(.init(unit))
        default:
            return nil
        }
    }
}

extension AttributeScopes.FoundationAttributes.ByteCountAttribute.Unit {
    internal init(_ unit: ByteCountFormatStyle.Unit) {
        switch unit {
        case .byte:
            self = .byte
        case .kilobyte:
            self = .kb
        case .megabyte:
            self = .mb
        case .gigabyte:
            self = .gb
        case .terabyte:
            self = .tb
        case .petabyte:
            self = .pb
        case .exabyte:
            self = .eb
        case .zettabyte:
            self = .zb
        case .yottabyte:
            self = .yb
        }
    }
}