Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalUI/FormatStyles/OWSByteCountFormatStyle.swift
1 views
//
// 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..<exponentiation {
            byteCount *= 1000
        }
        return byteCount
    }
}