File: Duration%2BUnitsFormatStyle.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 (775 lines) | stat: -rw-r--r-- 39,843 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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
//===----------------------------------------------------------------------===//
//
// 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 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension Duration {

    /// A `FormatStyle` that displays a duration as a list of duration units, such as "2 hours, 43 minutes, 26 seconds" in English.
    @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
    public struct UnitsFormatStyle : FormatStyle, Sendable {

        /// Specifies the width of the unit and the spacing of the value and the unit.
        public struct UnitWidth : Codable, Hashable, Sendable {
            var width: Measurement<UnitDuration>.FormatStyle.UnitWidth
            var patternStyle: UATimeUnitStyle
            
            private init(width: Measurement<UnitDuration>.FormatStyle.UnitWidth, patternStyle: UATimeUnitStyle) {
                self.width = width
                self.patternStyle = patternStyle
            }

            /// Shows the full unit name, such as "3 hours" for a 3-hour duration in the en_US locale.
            public static var wide: UnitWidth { .init(width: .wide, patternStyle: UATIMEUNITSTYLE_FULL) }

            /// Shows the abbreviated unit name, such as "3 hr" for a 3-hour duration in the en_US locale.
            public static var abbreviated: UnitWidth { .init(width: .abbreviated, patternStyle: UATIMEUNITSTYLE_ABBREVIATED) }

            /// Shows the abbreviated unit name with a condensed space between the value and unit, such as "3hr" for a 3-hour duration in the en_US locale.
            public static var condensedAbbreviated: UnitWidth { .init(width: .abbreviated, patternStyle: UATIMEUNITSTYLE_SHORTER) }

            /// Shows the shortest unit name, such as "3h" for a 3-hour duration in the en_US locale.
            public static var narrow: UnitWidth { .init(width: .narrow, patternStyle: UATIMEUNITSTYLE_NARROW) }
            
            private enum CodingKeys: CodingKey {
                case width
                case patternStyle
            }
            
            public init(from decoder: any Decoder) throws {
                let container = try decoder.container(keyedBy: CodingKeys.self)
                self.width = try container.decode(Measurement<UnitDuration>.FormatStyle.UnitWidth.self, forKey: .width)
                let rawValue = try container.decode(UATimeUnitStyle.RawValue.self, forKey: .patternStyle)
                self.patternStyle = UATimeUnitStyle(rawValue)
            }
            
            public func encode(to encoder: any Encoder) throws {
                var container = encoder.container(keyedBy: CodingKeys.self)
                try container.encode(self.width, forKey: .width)
                try container.encode(self.patternStyle.rawValue, forKey: .patternStyle)
            }
            
            public func hash(into hasher: inout Hasher) {
                hasher.combine(width)
                hasher.combine(patternStyle.rawValue)
            }
        }

        /// Units that a duration can be displayed as with `UnitsFormatStyle`.
        public struct Unit : Codable, Hashable, Sendable {
            // Sorted from largest to smallest
            enum _Unit : Int, Codable, Hashable, Comparable, CaseIterable {
                static func < (lhs: Duration.UnitsFormatStyle.Unit._Unit, rhs: Duration.UnitsFormatStyle.Unit._Unit) -> Bool {
                    // It's more intuitive to represent comparison by the unit duration, i.e. .weeks > .hours
                    // But the raw value is ordered the other way; reverse the comparison
                    lhs.rawValue > rhs.rawValue
                }

                case weeks
                case days
                case hours
                case minutes
                case seconds
                case milliseconds
                case microseconds
                case nanoseconds
            }
            var unit: _Unit
            var icuSkeleton: String {
                var subtype: String
                switch unit {
                case .weeks:
                    subtype = "week"
                case .days:
                    subtype = "day"
                case .hours:
                    subtype = "hour"
                case .minutes:
                    subtype = "minute"
                case .seconds:
                    subtype = "second"
                case .milliseconds:
                    subtype = "millisecond"
                case .microseconds:
                    subtype = "microsecond"
                case .nanoseconds:
                    subtype = "nanosecond"
                }
                return "measure-unit/duration-\(subtype)"
            }

            /// The unit for weeks. One week is always 604800 seconds.
            public static var weeks: Unit { .init(unit: .weeks) }

            /// The unit for days. One day is always 86400 seconds.
            public static var days: Unit { .init(unit: .days) }

            /// The unit for hours. One day is 3600 seconds.
            public static var hours: Unit { .init(unit: .hours) }

            /// The unit for minutes. One minute is 60 seconds.
            public static var minutes: Unit { .init(unit: .minutes) }

            /// The unit for seconds.
            public static var seconds: Unit { .init(unit: .seconds) }

            /// The unit for milliseconds.
            public static var milliseconds: Unit { .init(unit: .milliseconds) }

            /// The unit for microseconds.
            public static var microseconds: Unit { .init(unit: .microseconds) }

            /// The unit for nanoseconds.
            public static var nanoseconds: Unit { .init(unit: .nanoseconds) }
        }

        /// Specifies how zero value units are handled.
        public struct ZeroValueUnitsDisplayStrategy : Codable, Hashable, Sendable {
            var length: Int

            /// Excludes zero-value units from the formatted string.
            public static var hide: ZeroValueUnitsDisplayStrategy { .init(length: 0) }

            /// Displays zero-value units with zero padding to the specified length.
            public static func show(length: Int) -> ZeroValueUnitsDisplayStrategy { .init(length: length)}
        }

        /// Specifies how a duration is displayed if it cannot be represented exactly with the allowed units.
        ///
        /// For example, you can change this option to show a duration of 1 hour and 15 minutes as "1.25 hr", "1 hr", or "1.5 hr" with different lengths and rounding rules when hour is the only allowed unit.
        public struct FractionalPartDisplayStrategy : Codable, Hashable, Sendable {
            public var minimumLength: Int
            public var maximumLength: Int
            public var roundingRule: FloatingPointRoundingRule
            public var roundingIncrement: Double?

            init(minimumLength: Int, maximumLength: Int, roundingRule: FloatingPointRoundingRule, roundingIncrement: Double?) {
                self.minimumLength = minimumLength
                self.maximumLength = maximumLength
                self.roundingRule = roundingRule
                self.roundingIncrement = roundingIncrement
            }

            /// Displays the remaining part as the fractional part of the smallest unit.
            /// - Parameters:
            ///   - lengthLimits: The range of the length of the fractional part.
            ///   - roundingRule: Rounding rule for the remaining value.
            ///   - roundingIncrement: Rounding increment for the remaining value.
            public init<Range: RangeExpression>(lengthLimits: Range, roundingRule: FloatingPointRoundingRule = .toNearestOrEven, roundingIncrement: Double? = nil) where Range.Bound == Int {
                let (lower, upper) = lengthLimits.clampedLowerAndUpperBounds(0..<Int.max)
                self.init(minimumLength: lower ?? 0, maximumLength: upper ?? Int.max, roundingRule: roundingRule, roundingIncrement: roundingIncrement)
            }

            /// Displays the remaining part as the fractional part of the smallest unit.
            /// - Parameters:
            ///   - length: The length of the fractional part.
            ///   - rule: Rounding rule for the remaining value.
            ///   - increment: Rounding increment for the remaining value.
            public static func show(length: Int, rounded rule: FloatingPointRoundingRule = .toNearestOrEven, increment: Double? = nil) -> FractionalPartDisplayStrategy {
                .init(minimumLength: length, maximumLength: length, roundingRule: rule, roundingIncrement: increment)
            }

            /// Excludes the remaining part.
            public static var hide: FractionalPartDisplayStrategy {
                .init(minimumLength: 0, maximumLength: 0, roundingRule: .toNearestOrEven, roundingIncrement: nil)
            }

            /// Excludes the remaining part with the specified rounding rule.
            /// - Parameter rounded: Rounding rule for the remaining value.
            public static func hide(rounded: FloatingPointRoundingRule = .toNearestOrEven) -> FractionalPartDisplayStrategy {
                .init(minimumLength: 0, maximumLength: 0, roundingRule: rounded, roundingIncrement: nil)
            }

        }

        /// The locale to use when formatting the duration.
        public var locale: Locale

        /// The units that may be included in the output string.
        public var allowedUnits: Set<Unit>

        /// The width of the unit and the spacing between the value and the unit.
        public var unitWidth: UnitWidth

        /// The maximum number of time units to include in the output string.
        public var maximumUnitCount: Int?

        /// The strategy for how zero-value units are handled.
        public var zeroValueUnitsDisplay: ZeroValueUnitsDisplayStrategy

        /// The strategy for displaying a duration if it cannot be represented exactly with the allowed units.
        public var fractionalPartDisplay: FractionalPartDisplayStrategy

        /// The padding or truncating behavior of the unit value.
        ///
        /// For example, set this to `2...` to force 2-digit padding on all units.
        public var valueLengthLimits: Range<Int>?

        /// Creates an instance using the provided specifications.
        /// - Parameters:
        ///   - allowedUnits: The units that may be included in the output string.
        ///   - width: The width of the unit and the spacing between the value and the unit.
        ///   - maximumUnitCount: The maximum number of time units to include in the output string.
        ///   - zeroValueUnits: The strategy for how zero-value units are handled.
        ///   - valueLength: The padding or truncating behavior of the unit value. Negative values are ignored.
        ///   - fractionalPart: The strategy for displaying a duration if it cannot be represented exactly with the allowed units.
        public init(allowedUnits: Set<Unit>, width: UnitWidth, maximumUnitCount: Int? = nil, zeroValueUnits: ZeroValueUnitsDisplayStrategy = .hide, valueLength: Int? = nil, fractionalPart: FractionalPartDisplayStrategy = .hide) {
            self.allowedUnits = allowedUnits
            self.unitWidth = width
            self.maximumUnitCount = maximumUnitCount
            self.zeroValueUnitsDisplay = zeroValueUnits
            self.fractionalPartDisplay = fractionalPart
            if let valueLength, valueLength > 0 {
                let upperBound = min(Int.max - 1, valueLength)
                self.valueLengthLimits = upperBound ..< upperBound + 1
            } else {
                self.valueLengthLimits = nil
            }
            self.locale = .autoupdatingCurrent
        }

        /// Creates an instance using the provided specifications.
        /// - Parameters:
        ///   - allowedUnits: The units that may be included in the output string.
        ///   - width: The width of the unit and the spacing between the value and the unit.
        ///   - maximumUnitCount: The maximum number of time units to include in the output string.
        ///   - zeroValueUnits: The strategy for how zero-value units are handled.
        ///   - valueLengthLimits: The padding or truncating behavior of the unit value. Values with negative bounds are ignored.
        ///   - fractionalPart: The strategy for displaying a duration if it cannot be represented exactly with the allowed units.
        public init<ValueRange: RangeExpression>(allowedUnits: Set<Unit>, width: UnitWidth, maximumUnitCount: Int? = nil, zeroValueUnits: ZeroValueUnitsDisplayStrategy = .hide, valueLengthLimits: ValueRange, fractionalPart: FractionalPartDisplayStrategy = .hide) where ValueRange.Bound == Int {
            self.allowedUnits = allowedUnits
            self.unitWidth = width
            self.maximumUnitCount = maximumUnitCount
            self.zeroValueUnitsDisplay = zeroValueUnits
            self.fractionalPartDisplay = fractionalPart
            let (lower, upper) = valueLengthLimits.clampedLowerAndUpperBounds(0..<Int.max)
            if lower == nil && upper == nil {
                self.valueLengthLimits = nil
            } else {
                self.valueLengthLimits = (lower ?? 0) ..< (upper ?? Int.max)
            }

            self.locale = .autoupdatingCurrent
        }

        // MARK: - `FormatStyle` conformance

        /// Creates a locale-aware string representation from a duration value.
        /// - Parameter duration: The value to format.
        /// - Returns: A string representation of the duration.
        public func format(_ duration: Duration) -> String {
            let formattedFields = _formatFields(duration)
            var result = _getFullListPattern(length: formattedFields.count)
            for formattedField in formattedFields.reversed() {
                let range = result._range(of: "{0}", anchored: false, backwards: true)!
                result.replaceSubrange(range, with: formattedField)
            }
            return result
        }

        // The number format does not contain rounding settings because it's handled on the value itself
        func _createNumberFormatStyle(useFractionalLimitsIfAvailable: Bool) -> FloatingPointFormatStyle<Double> {
            var collection = NumberFormatStyleConfiguration.Collection()

            let fractionalLimits = useFractionalLimitsIfAvailable ? fractionalPartDisplay.minimumLength...fractionalPartDisplay.maximumLength : 0...0
            let zeroValueLimits = zeroValueUnitsDisplay.length...
            if let valueLengthLimits = valueLengthLimits, zeroValueUnitsDisplay.length > 0 {
                let tightestLimits = zeroValueLimits.relative(to: valueLengthLimits)
                collection.precision = .integerAndFractionLength(integerLimits: tightestLimits, fractionLimits: fractionalLimits)
            } else if let valueLengthLimits = valueLengthLimits {
                collection.precision = .integerAndFractionLength(integerLimits: valueLengthLimits, fractionLimits: fractionalLimits)
            } else if zeroValueUnitsDisplay.length > 0 {
                collection.precision = .integerAndFractionLength(integerLimits: zeroValueLimits, fractionLimits: fractionalLimits)
            } else {
                collection.precision = .fractionLength(fractionalLimits)
            }

            var format = FloatingPointFormatStyle<Double>(locale: locale)
            format.collection = collection

            return format
        }

        func _formatFields(_ duration: Duration) -> [String] {
            let skeletons = _getSkeletons(duration)
            return skeletons.map { (skeleton: String, unit: Unit, value: Double) in
                let numberFormatter = ICUMeasurementNumberFormatter.create(for: skeleton, locale: locale)!
                let formatted = numberFormatter.format(value)
                return formatted ?? "\(value) \(unit.icuSkeleton)" // Return a description if ICU can't format it
            }
        }

        func _getSkeletons(_ duration: Duration) -> [(skeleton: String, measurementUnit: Unit, measurementValue: Double)] {

            let (units, values) = Self.unitsToUse(duration: duration, allowedUnits: allowedUnits, maximumUnitCount: maximumUnitCount, roundSmallerParts: fractionalPartDisplay.roundingRule, trailingFractionalPartLength: fractionalPartDisplay.maximumLength, roundingIncrement: fractionalPartDisplay.roundingIncrement, dropZeroUnits: zeroValueUnitsDisplay.length <= 0)

            let numberFormatStyleWithFraction = _createNumberFormatStyle(useFractionalLimitsIfAvailable: true)
            let numberFormatStyleNoFraction = _createNumberFormatStyle(useFractionalLimitsIfAvailable: false)

            if units.count == 0, let smallest = allowedUnits.min(by: { $0.unit.rawValue > $1.unit.rawValue }) {
                // Fallback to the smallest allowed unit when there is no units to show, such as when the duration is 0 and client wants to hide zero fields

                let skeleton = ICUMeasurementNumberFormatter.skeleton(smallest.icuSkeleton, width: .init(unitWidth), usage: nil, numberFormatStyle: numberFormatStyleWithFraction)

                return [(skeleton, measurementUnit: smallest, measurementValue: 0)]
            }

            var result = [(skeleton: String, measurementUnit: Unit, measurementValue: Double)]()

            let isNegative = values.contains(where: { $0 < 0 })

            lazy var mostSignificantUnit = units.map(\.unit).max()

            for (index, (unit, value)) in zip(units, values).enumerated() {
                var numberFormatStyle: FloatingPointFormatStyle<Double>
                if index == units.count - 1 {
                    numberFormatStyle = numberFormatStyleWithFraction
                } else {
                    numberFormatStyle = numberFormatStyleNoFraction
                }

                var value = value
                // we only want the sign to show for the first component
                // and only if the overall value is negative
                if isNegative && unit.unit == mostSignificantUnit {
                    numberFormatStyle = numberFormatStyle.sign(strategy: .always(includingZero: true))
                    // if the first component is zero, we normally wouldn't get
                    // a negative sign, so we make the value a small negative
                    // value that still rounds to zero
                    if value == .zero {
                        value = -0.1
                    }
                } else {
                    numberFormatStyle = numberFormatStyle.sign(strategy: .never)
                }


                let skeleton = ICUMeasurementNumberFormatter.skeleton(unit.icuSkeleton, width: .init(unitWidth), usage: nil, numberFormatStyle: numberFormatStyle)

                result.append((skeleton: skeleton, measurementUnit: unit, measurementValue: value))
            }

            return result
        }

        func _getListPattern(_ type: UATimeUnitListPattern) -> String {
            let listPattern = _withFixedUCharBuffer(size: 128) { buffer, size, status in
                uatmufmt_getListPattern(locale.identifier, unitWidth.patternStyle, type, buffer, size, &status)
            }

            if let listPattern {
                return listPattern
            } else {
                let fallbackPattern = "{0}, {1}"
                return fallbackPattern
            }
        }

        // A list pattern has a form such as
        //
        // unit-short{
        //    2{"{0}, {1}"}
        //    end{"{0}, {1}"}
        //    middle{"{0}, {1}"}
        //    start{"{0}, {1}"}
        // }
        //
        // Returns a "combined list pattern" that contains all the start, middle and end parts. The returned pattern uses "{0}" as the placeholder. The result looks something like this: "{0}, {0}, {0}, and {0}"
        func _getFullListPattern(length: Int) -> String {
            let placeholder = "{0}"
            let lastPlaceholder = "{1}"

            var pattern: String!

            switch length {
            case 1:
                pattern = placeholder
            case 2:
                pattern = self._getListPattern(UATIMEUNITLISTPAT_TWO_ONLY)
                pattern.replace(lastPlaceholder, with: placeholder)
            case let length:
                let middle = self._getListPattern(UATIMEUNITLISTPAT_MIDDLE_PIECE)

                pattern = self._getListPattern(UATIMEUNITLISTPAT_START_PIECE)
                // Each of the three pieces provides _two_ placeholders each,
                // such that we start with two and each replacement adds one
                // more, so start the loop at 2 as well.
                for _ in 2 ..< (length - 1) {
                    pattern.replace(lastPlaceholder, with: middle)
                }

                pattern.replace(lastPlaceholder, with: self._getListPattern(UATIMEUNITLISTPAT_END_PIECE))
                pattern.replace(lastPlaceholder, with: placeholder)
            }
            return pattern
        }

        static func removingZeroUnits(units: [Unit], values: [Double]) -> (units: [Unit], values: [Double]) {
            var nonZeroUnits = [Unit]()
            var nonZeroValues = [Double]()
            for (idx, value) in values.enumerated() {
                if value != 0 {
                    nonZeroUnits.append(units[idx])
                    nonZeroValues.append(value)
                }
            }
            return (nonZeroUnits, nonZeroValues)
        }

        // Returns the units that are going to show up in the final string, sorted from largest to smallest
        static func unitsToUse(duration: Duration, allowedUnits: Set<Unit>, maximumUnitCount: Int?, roundSmallerParts: FloatingPointRoundingRule, trailingFractionalPartLength: Int, roundingIncrement: Double?, dropZeroUnits: Bool) -> (units: [Unit], values: [Double]) {

            var units = allowedUnits.sorted { $0.unit.rawValue < $1.unit.rawValue }
            var values = duration.valuesForUnits(units, trailingFractionalLength: trailingFractionalPartLength, smallestUnitRounding: roundSmallerParts, roundingIncrement: roundingIncrement)

            // First check if we fit in `maximumUnitCount`
            if maximumUnitCount == nil || allowedUnits.count <= maximumUnitCount! {
                return dropZeroUnits ? removingZeroUnits(units: units, values: values) : (units, values)
            }

            let maximumUnitCount = maximumUnitCount!

            // If we can drop zero fields, check if the units fit in after dropping them
            if dropZeroUnits {
                let (nonZeroUnits, nonZeroValues) = removingZeroUnits(units: units, values: values)
                if nonZeroUnits.count <= maximumUnitCount {
                    return (nonZeroUnits, nonZeroValues)
                } else {
                    units = nonZeroUnits
                    values = nonZeroValues
                }
            }

            // We can't drop zero fields, or dropping them still exceeds `maximumUnitCount`
            // Move on to collapse units to fit into `maximumUnitCount`
            let idx = values.firstIndex { $0 != 0 }
            guard let idx = idx else {
                // Cannot generate a list of units using `allowedUnits`.
                return (units, values)
            }

            let r = idx ..< min(units.count, idx + maximumUnitCount)
            let usefulUnits = Array(units[r])
            let usefulValues = duration.valuesForUnits(usefulUnits, trailingFractionalLength: trailingFractionalPartLength, smallestUnitRounding: roundSmallerParts, roundingIncrement: roundingIncrement)

            return (usefulUnits, usefulValues)
        }

        /// A modifier to set the locale of the format style.
        /// - Parameter locale: The locale to apply to the format style.
        /// - Returns: A copy of this format with the new locale set.
        public func locale(_ locale: Locale) -> Self {
            var new = self
            new.locale = locale
            return new
        }

        /// Returns a `Duration.UnitsFormatStyle.Attributed` style to format a duration as an attributed string using the configuration of this format style. Units in the string are annotated with the `durationField` and `measurement` attribute keys and the `DurationFieldAttribute` and `MeasurementAttribute` attribute values.
        ///
        /// For example, formatting a duration of 2 hours, 43 minutes, 26.25 second in `en_US` locale yeilds the following conceptually
        /// ```
        /// 2 { durationField: .hours, component: .value }
        /// hours { durationField: .hours, component: .unit }
        /// , { nil }
        /// 43 { durationField: .minutes, component: .value }
        /// minutes { durationField: .minutes, component: .unit }
        /// , { nil }
        /// 26.25 { durationField: .seconds, component: .value }
        /// seconds { durationField: .seconds, component: .unit }
        /// ```
        public var attributed: Attributed {
            Attributed(innerStyle: self)
        }
    }

    // For testing purpose. See notes about String._Encoding
    internal typealias _UnitsFormatStyle = UnitsFormatStyle
}

// `FormatStyle` static membership lookup
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension FormatStyle where Self == Duration.UnitsFormatStyle {
    /// A factory function to create a units format style to format a duration.
    /// - Parameters:
    ///   - units: The units that may be included in the output string.
    ///   - width: The width of the unit and the spacing between the value and the unit.
    ///   - maximumUnitCount: The maximum number of time units to include in the output string.
    ///   - zeroValueUnits: The strategy for how zero-value units are handled.
    ///   - valueLength: The padding or truncating behavior of the unit value.
    ///   - fractionalPart: The strategy for displaying a duration if it cannot be represented exactly with the allowed units.
    /// - Returns: A format style to format a duration.
    public static func units(allowed units: Set<Duration.UnitsFormatStyle.Unit> = [.hours, .minutes, .seconds], width: Duration.UnitsFormatStyle.UnitWidth = .abbreviated, maximumUnitCount : Int? = nil, zeroValueUnits: Duration.UnitsFormatStyle.ZeroValueUnitsDisplayStrategy = .hide, valueLength: Int? = nil, fractionalPart: Duration.UnitsFormatStyle.FractionalPartDisplayStrategy = .hide) -> Self {
        .init(allowedUnits: units, width: width, maximumUnitCount: maximumUnitCount, zeroValueUnits: zeroValueUnits, valueLength: valueLength, fractionalPart: fractionalPart)
    }

    /// A factory function to create a units format style to format a duration.
    /// - Parameters:
    ///   - allowedUnits: The units that may be included in the output string.
    ///   - width: The width of the unit and the spacing between the value and the unit.
    ///   - maximumUnitCount: The maximum number of time units to include in the output string.
    ///   - zeroValueUnits: The strategy for how zero-value units are handled.
    ///   - valueLengthLimits: The padding or truncating behavior of the unit value.
    ///   - fractionalPart: The strategy for displaying a duration if it cannot be represented exactly with the allowed units.
    ///   - Returns: A format style to format a duration.
    public static func units<ValueRange: RangeExpression>(allowed units: Set<Duration.UnitsFormatStyle.Unit> = [.hours, .minutes, .seconds], width: Duration.UnitsFormatStyle.UnitWidth = .abbreviated, maximumUnitCount : Int? = nil, zeroValueUnits: Duration.UnitsFormatStyle.ZeroValueUnitsDisplayStrategy = .hide, valueLengthLimits: ValueRange, fractionalPart: Duration.UnitsFormatStyle.FractionalPartDisplayStrategy = .hide) -> Self where ValueRange.Bound == Int {
        .init(allowedUnits: units, width: width, maximumUnitCount: maximumUnitCount, zeroValueUnits: zeroValueUnits, valueLengthLimits: valueLengthLimits, fractionalPart: fractionalPart)
    }
}

// MARK: - Attributed style

@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
extension Duration.UnitsFormatStyle {

    /// A format style to format a duration as an attributed string. Units in the string are annotated with the `durationField` and `measurement` attribute keys and the `DurationFieldAttribute` and `MeasurementAttribute` attribute values.
    ///
    /// You can use `Duration.UnitsFormatStyle` to configure the style, and create an `Attributed` format with its `public var attributed: Attributed`
    ///
    /// For example, formatting a duration of 2 hours, 43 minutes, 26.25 second in `en_US` locale yeilds the following conceptually
    /// ```
    /// 2 { durationField: .hours, component: .value }
    /// hours { durationField: .hours, component: .unit }
    /// , { nil }
    /// 43 { durationField: .minutes, component: .value }
    /// minutes { durationField: .minutes, component: .unit }
    /// , { nil }
    /// 26.25 { durationField: .seconds, component: .value }
    /// seconds { durationField: .seconds, component: .unit }
    /// ```
    @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
    @dynamicMemberLookup
    public struct Attributed : FormatStyle, Sendable {

        var innerStyle: Duration.UnitsFormatStyle

        /// Formats a duration as an attributed string with `DurationFieldAttribute`.
        public func format(_ duration: Duration) -> AttributedString {
            let formattedFields = _formatFields(duration)
            var result = AttributedString(innerStyle._getFullListPattern(length: formattedFields.count))
            for formattedField in formattedFields.reversed() {
                let range = result.range(of: "{0}", options: [.backwards])!
                result.replaceSubrange(range, with: formattedField)
            }

            return result
        }

        /// A modifier to set the locale of the format style.
        /// - Parameter locale: The locale to apply to the format style.
        /// - Returns: A copy of this format with the new locale set.
        public func locale(_ locale: Locale) -> Self {
            Attributed(innerStyle: innerStyle.locale(locale))
        }

        func _formatFields(_ duration: Duration) -> [AttributedString] {
            typealias Component = AttributeScopes.FoundationAttributes.MeasurementAttribute.Component
            typealias DurationField = AttributeScopes.FoundationAttributes.DurationFieldAttribute.Field

            let skeletons = innerStyle._getSkeletons(duration)
            return skeletons.map { (skeleton: String, unit: Unit, value: Double) in
                let numberFormatter = ICUMeasurementNumberFormatter.create(for: skeleton, locale: innerStyle.locale)!
                var durationField: DurationField!
                switch unit.unit {
                case .weeks:
                    durationField = .weeks
                case .days:
                    durationField = .days
                case .hours:
                    durationField = .hours
                case .minutes:
                    durationField = .minutes
                case .seconds:
                    durationField = .seconds
                case .milliseconds:
                    durationField = .milliseconds
                case .microseconds:
                    durationField = .microseconds
                case .nanoseconds:
                    durationField = .nanoseconds
                }

                guard let (str, attributes) = numberFormatter.attributedFormatPositions(.floatingPoint(value)) else {
                    return AttributedString(innerStyle.format(duration), attributes: .init().durationField(durationField))
                }

                var attrStr = AttributedString(str)
                attrStr.durationField = durationField

                for attr in attributes {
                    var component: Component?
                    switch attr.field {
                    case .measureUnit:
                        component = .unit
                    default:
                        component = .value
                    }

                    let strRange = String.Index(utf16Offset: attr.begin, in: str)..<String.Index(utf16Offset: attr.end, in: str)
                    if let range = Range(strRange, in: attrStr) {
                        attrStr[range].measurement = component
                    }

                }
                return attrStr
            }
        }
    }
}

// MARK: Dynamic Member Lookup

@available(FoundationPreview 0.4, *)
extension Duration.UnitsFormatStyle.Attributed {
    public subscript<T>(dynamicMember key: KeyPath<Duration.UnitsFormatStyle, T>) -> T {
        innerStyle[keyPath: key]
    }

    public subscript<T>(dynamicMember key: WritableKeyPath<Duration.UnitsFormatStyle, T>) -> T {
        get {
            innerStyle[keyPath: key]
        }
        set {
            innerStyle[keyPath: key] = newValue
        }
    }
}

// MARK: DiscreteFormatStyle Conformance

@available(FoundationPreview 0.4, *)
extension Duration.UnitsFormatStyle.Attributed : DiscreteFormatStyle {
    public func discreteInput(before input: Duration) -> Duration? {
        self.innerStyle.discreteInput(before: input)
    }

    public func discreteInput(after input: Duration) -> Duration? {
        self.innerStyle.discreteInput(after: input)
    }
}

@available(FoundationPreview 0.4, *)
extension Duration.UnitsFormatStyle : DiscreteFormatStyle {
    public func discreteInput(before input: Duration) -> Duration? {
        let (bound, isIncluded) = self.bound(for: input, countingDown: true)

        return isIncluded ? bound.nextDown : bound
    }

    public func discreteInput(after input: Duration) -> Duration? {
        let (bound, isIncluded) = self.bound(for: input, countingDown: false)

        return isIncluded ? bound.nextUp : bound
    }

    private func bound(for input: Duration, countingDown: Bool) -> (bound: Duration, includedInRangeOfInput: Bool) {
        // Initially we determine the interval for the smallest unit that is used to
        // format `input`. If `forceRoundingToFull` is true, that is because we are
        // rounding `toNearestOr` and we are close to the point where `interval`
        // changes.
        let (interval, forceRoundingToFull) = interval(for: input,
                                                       countingDown: countingDown,
                                                       allowedUnits: self.allowedUnits)

        // Thus, if `forceRoundingToFull` is true, we round `.towardZero`. E.g.
        // if we can only show one unit and we're at -70 seconds, we format that
        // as "1 minute". By rounding `.towardZero`, we get 60 seconds as the
        // `unadjustedBound`, not 30 seconds as we would get for `toNearestOr`
        // rounding.
        let (unadjustedBound, includedInRangeOfInput) = Duration.bound(for: input,
                                                                       in: interval,
                                                                       countingDown: countingDown,
                                                                       roundingRule: forceRoundingToFull ? .towardZero : self.fractionalPartDisplay.roundingRule)

        // If we didn't `forceRoundingToFull`, we're done at this point. However,
        // if we did, we determine the bound again, disallowing the unit that
        // would just fit the `unadjustedBound` (in the example `.minute`), so
        // we get the appropriate bound for the smaller unit, which would be
        // 59.5 seconds in the example, rendered as "59 seconds".
        if forceRoundingToFull {
            let (bound, includedInRangeOfInput) = Duration.bound(for: unadjustedBound,
                                                                    in: self.interval(for: unadjustedBound,
                                                                                      countingDown: countingDown,
                                                                                      allowedUnits: allowedUnits.filter({ Duration.interval(for: $0) < abs(unadjustedBound) })).duration,
                                                                    countingDown: countingDown,
                                                                    roundingRule: self.fractionalPartDisplay.roundingRule)

            return (bound, includedInRangeOfInput)
        } else {
            return (unadjustedBound, includedInRangeOfInput)
        }
    }

    private func interval(for input: Duration, countingDown: Bool, allowedUnits: Set<Unit>) -> (duration: Duration, forceRoundingToFull: Bool) {
        let allowedUnits = Unit._Unit.allCases.filter({ allowedUnits.contains(.init(unit: $0)) }).map({ Unit(unit: $0) })

        guard let smallestAllowedUnit = allowedUnits.last else {
            return (.seconds(Int64.max), false)
        }

        var remainder = input
        var visibleUnitLimit = self.maximumUnitCount ?? allowedUnits.count
        var smallestInterval: Duration!
        var forceRoundingToFull = false

        let roundsToHalf = self.fractionalPartDisplay.roundingRule == .toNearestOrEven || self.fractionalPartDisplay.roundingRule == .toNearestOrAwayFromZero

        for unit in allowedUnits {
            guard visibleUnitLimit > 0 else {
                break
            }

            let unitInterval = Duration.interval(for: unit)

            let roundedRemainder = input.rounded(increment: Duration.interval(for: smallestAllowedUnit,
                                                                       fractionalDigits: self.fractionalPartDisplay.maximumLength,
                                                                       roundingIncrement: self.fractionalPartDisplay.roundingIncrement),
                                          rule: self.fractionalPartDisplay.roundingRule)

            guard unit == smallestAllowedUnit || unitInterval < abs(roundedRemainder) || unitInterval == abs(roundedRemainder) && (remainder < .zero) == countingDown else {
                continue
            }



            var interval: Duration
            if unit == smallestAllowedUnit || visibleUnitLimit == 1  {
                interval = Duration.interval(for: unit, fractionalDigits: self.fractionalPartDisplay.maximumLength, roundingIncrement: self.fractionalPartDisplay.roundingIncrement)
            } else {
                interval = Duration.interval(for: unit)
            }

            if roundsToHalf && countingDown == (remainder > .zero) && abs(remainder) <= unitInterval + interval / 2 && unit != smallestAllowedUnit && visibleUnitLimit == 1 {
                forceRoundingToFull = true
            } else {
                forceRoundingToFull = false
            }

            let value = roundedRemainder.rounded(increment: interval, rule: .towardZero)

            remainder -= value

            if value != .zero || self.zeroValueUnitsDisplay.length > 0 {
                visibleUnitLimit -= 1
            }

            smallestInterval = interval
        }


        return (smallestInterval, forceRoundingToFull)
    }
}