// // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // public struct OWSByteCountFormatStyle: FormatStyle { private let fudgeBase2ToBase10: Bool private let zeroPadFractionDigits: Bool /// - Parameter fudgeBase2ToBase10 /// Whether the given byte count should be "fudged" from a base2 to base10 /// value. See `OWSBase2ByteCountFudger` for more. /// - Parameter zeroPadFractionDigits /// Whether the formatted string should zero-pad fraction digits to maintain /// a consistent string length across multiple formatting instances. Callers /// formatting a single fixed value, rather than a value that may change /// in-place, may prefer to pass `false` to avoid unnecessary zero-padding. public init( fudgeBase2ToBase10: Bool = false, zeroPadFractionDigits: Bool = true, ) { self.fudgeBase2ToBase10 = fudgeBase2ToBase10 self.zeroPadFractionDigits = zeroPadFractionDigits } public func format(_ byteCountParam: UInt64) -> String { let byteCount: UInt64 if fudgeBase2ToBase10, let fudged = OWSBase2ByteCountFudger.fudgeBase2ToBase10(byteCountParam) { byteCount = fudged } else { byteCount = byteCountParam } let byteFormatter = ByteCountFormatter() // Use KB, MB, GB, etc as appropriate. byteFormatter.allowedUnits = .useAll // Assume the byte count is base-10, i.e. "1000 bytes / 1 KB". See // OWSBase2ByteCountFudger for more. byteFormatter.countStyle = .decimal // Don't use, for example, the word "zero" instead of the numeral "0". byteFormatter.allowsNonnumericFormatting = false // Zero-pad fractions to keep a fixed number of digits in the string, if // we format multiple different values that might otherwise produce // different fractional lengths. Otherwise, it can look "jumpy"; e.g., // going from 400 MB to 400.1 MB. // // The number of digits is managed by the formatter: see `isAdaptive`, // which defaults to true. byteFormatter.zeroPadsFractionDigits = zeroPadFractionDigits return byteFormatter.string(fromByteCount: Int64(clamping: byteCount)) } } extension FormatStyle where Self == OWSByteCountFormatStyle { public static func owsByteCount( fudgeBase2ToBase10: Bool = false, zeroPadFractionDigits: Bool = true, ) -> OWSByteCountFormatStyle { return OWSByteCountFormatStyle( fudgeBase2ToBase10: fudgeBase2ToBase10, zeroPadFractionDigits: zeroPadFractionDigits, ) } } // MARK: - enum OWSBase2ByteCountFudger { /// If the given byte count is a multiple of an exponentiation of 1024, /// returns the byte count as a multiple of the corresponding exponentiation /// of 1000 instead. In other words, fudges base-2 byte counts to their /// roughly-approximate base-10 byte count instead. /// /// For example, given the value `107_374_182_400` (or `100 * 1024^3`, aka /// "100 gibibytes/GiB"), returns `100_000_000_000` (or `100 * 1000^3`, aka /// "100 gigabytes/GB"). /// /// See https://simple.wikipedia.org/wiki/Gibibyte if you, like me, were /// surprised to learn about base-2 byte values. /// /// - Note /// At the time of writing, the use case for this is the Backups /// `storageAllowanceBytes` value, which is configured on the server to be a /// GiB value rather than GB. static func fudgeBase2ToBase10(_ byteCount: UInt64) -> UInt64? { if byteCount == 0 { return 0 } // Cut byteCount down by 1024s until it can't be cut anymore. At that // point it'll represent the "multiple"; e.g., we were given 37 MiB and // are left with 37. var byteCount = byteCount var exponentiation = 0 while byteCount >= 1024 { if byteCount % 1024 != 0 { // In the end, not roundly divisible by an exponentiation of // 1024. Bail. return nil } byteCount /= 1024 exponentiation += 1 } // Then, build it back up using 1000s. // // This can't overflow since 1000 < 1024, so the resulting value will // always be smaller than the passed-in value. for _ in 0..