mirror of
https://github.com/signalapp/Signal-iOS.git
synced 2025-12-05 01:10:41 +00:00
Add OWSBase2ByteCountFudger to format GiB as GB
This commit is contained in:
@@ -2803,6 +2803,7 @@
|
||||
D9C4C0852E0A75F200B74696 /* BackupAttachmentUploadEraStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C4C0842E0A75EF00B74696 /* BackupAttachmentUploadEraStore.swift */; };
|
||||
D9C544292B8578B50036F274 /* CallRecord+CallStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C544282B8578B50036F274 /* CallRecord+CallStatus.swift */; };
|
||||
D9C5442B2B8578F30036F274 /* CallRecordMissedCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C5442A2B8578F30036F274 /* CallRecordMissedCallManager.swift */; };
|
||||
D9C5634F2EB438500001626A /* OWSByteCountFormatStyleTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C5634D2EB438300001626A /* OWSByteCountFormatStyleTest.swift */; };
|
||||
D9C7CEB428EB8495001E87B6 /* ExperienceUpgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C7CEB328EB8495001E87B6 /* ExperienceUpgrade.swift */; };
|
||||
D9C7CECB28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C7CECA28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift */; };
|
||||
D9C7CECF28ECC043001E87B6 /* NSAttributedString+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C7CECE28ECC043001E87B6 /* NSAttributedString+SSK.swift */; };
|
||||
@@ -6789,6 +6790,7 @@
|
||||
D9C4C0842E0A75EF00B74696 /* BackupAttachmentUploadEraStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttachmentUploadEraStore.swift; sourceTree = "<group>"; };
|
||||
D9C544282B8578B50036F274 /* CallRecord+CallStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CallRecord+CallStatus.swift"; sourceTree = "<group>"; };
|
||||
D9C5442A2B8578F30036F274 /* CallRecordMissedCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRecordMissedCallManager.swift; sourceTree = "<group>"; };
|
||||
D9C5634D2EB438300001626A /* OWSByteCountFormatStyleTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSByteCountFormatStyleTest.swift; sourceTree = "<group>"; };
|
||||
D9C7CEB328EB8495001E87B6 /* ExperienceUpgrade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceUpgrade.swift; sourceTree = "<group>"; };
|
||||
D9C7CECA28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeManifest.swift; sourceTree = "<group>"; };
|
||||
D9C7CECE28ECC043001E87B6 /* NSAttributedString+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+SSK.swift"; sourceTree = "<group>"; };
|
||||
@@ -11240,6 +11242,7 @@
|
||||
05104D172C8A151100F8851F /* AsyncViewTask.swift */,
|
||||
05594CCF2C98A00200CCBFF6 /* HostingController.swift */,
|
||||
D9B2E1172E748DFB00A823E4 /* OWSByteCountFormatStyle.swift */,
|
||||
D9C5634D2EB438300001626A /* OWSByteCountFormatStyleTest.swift */,
|
||||
D9DE35002DF0B886005099D7 /* ScrollableContentPinnedFooterView.swift */,
|
||||
05594CCD2C989F1900CCBFF6 /* ScrollableWhenCompact.swift */,
|
||||
0510F69D2C91EB2800FA3FDE /* ScrollBounceBehaviorIfAvailable.swift */,
|
||||
@@ -16738,6 +16741,7 @@
|
||||
5073EACB2C4F3A16001FBB3E /* LinkPreviewFetcherTest.swift in Sources */,
|
||||
50BF510A2BB2031600C2C309 /* LinkPreviewFetchStateTest.swift in Sources */,
|
||||
50BF510C2BB2032500C2C309 /* MobileCoinHelperSDKTest.swift in Sources */,
|
||||
D9C5634F2EB438500001626A /* OWSByteCountFormatStyleTest.swift in Sources */,
|
||||
50BF510E2BB2033800C2C309 /* RecipientPickerViewControllerTest.swift in Sources */,
|
||||
50BF51102BB2035400C2C309 /* UIStackView+SignalUITest.swift in Sources */,
|
||||
);
|
||||
|
||||
@@ -1470,12 +1470,15 @@ struct BackupSettingsView: View {
|
||||
"BACKUP_SETTINGS_UPLOAD_PROGRESS_SUBTITLE_PAUSED_OUT_OF_STORAGE_SPACE_FORMAT",
|
||||
comment: "Subtitle for a progress bar tracking uploads that are paused because the user is out of remote storage space. Embeds 1:{{ total storage space provided, e.g. 100 GB }}; 2:{{ space the user needs to free up by deleting media, e.g. 1 GB }}."
|
||||
),
|
||||
viewModel.backupSubscriptionConfiguration.storageAllowanceBytes.formatted(.owsByteCount),
|
||||
viewModel.backupSubscriptionConfiguration.storageAllowanceBytes.formatted(.owsByteCount(
|
||||
fudgeBase2ToBase10: true,
|
||||
zeroPadFractionDigits: false,
|
||||
)),
|
||||
max(
|
||||
// Always display at least 5 MB
|
||||
1000 * 1000 * 5,
|
||||
Int64(clamping: mediaTierCapacityOverflow)
|
||||
).formatted(.owsByteCount)
|
||||
).formatted(.owsByteCount())
|
||||
)
|
||||
)
|
||||
.appendLink(CommonStrings.learnMore, useBold: true, tint: .Signal.label) {
|
||||
@@ -2016,7 +2019,7 @@ private struct BackupAttachmentDownloadProgressView: View {
|
||||
"BACKUP_SETTINGS_DOWNLOAD_PROGRESS_SUBTITLE_SUSPENDED",
|
||||
comment: "Subtitle for a view explaining that downloads are available but not running. Embeds {{ the amount available to download as a file size, e.g. 100 MB }}."
|
||||
),
|
||||
latestDownloadUpdate.totalBytesToDownload.formatted(.owsByteCount)
|
||||
latestDownloadUpdate.totalBytesToDownload.formatted(.owsByteCount())
|
||||
)
|
||||
case .disabling, .paidExpiringSoon:
|
||||
String(
|
||||
@@ -2024,7 +2027,7 @@ private struct BackupAttachmentDownloadProgressView: View {
|
||||
"BACKUP_SETTINGS_DOWNLOAD_PROGRESS_SUBTITLE_SUSPENDED_PAID_SUBSCRIPTION_EXPIRING",
|
||||
comment: "Subtitle for a view explaining that downloads are available but not running, and the user's paid subscription is expiring. Embeds {{ the amount available to download as a file size, e.g. 100 MB }}."
|
||||
),
|
||||
latestDownloadUpdate.totalBytesToDownload.formatted(.owsByteCount)
|
||||
latestDownloadUpdate.totalBytesToDownload.formatted(.owsByteCount())
|
||||
)
|
||||
}
|
||||
case .running:
|
||||
@@ -2033,8 +2036,8 @@ private struct BackupAttachmentDownloadProgressView: View {
|
||||
"BACKUP_SETTINGS_DOWNLOAD_PROGRESS_SUBTITLE_RUNNING",
|
||||
comment: "Subtitle for a progress bar tracking active downloading. Embeds 1:{{ the amount downloaded as a file size, e.g. 100 MB }}; 2:{{ the total amount to download as a file size, e.g. 1 GB }}; 3:{{ the amount downloaded as a percentage, e.g. 10% }}."
|
||||
),
|
||||
latestDownloadUpdate.bytesDownloaded.formatted(.owsByteCount),
|
||||
latestDownloadUpdate.totalBytesToDownload.formatted(.owsByteCount),
|
||||
latestDownloadUpdate.bytesDownloaded.formatted(.owsByteCount()),
|
||||
latestDownloadUpdate.totalBytesToDownload.formatted(.owsByteCount()),
|
||||
latestDownloadUpdate.percentageDownloaded.formatted(.percent.precision(.fractionLength(0))),
|
||||
)
|
||||
case .pausedLowBattery:
|
||||
@@ -2063,7 +2066,7 @@ private struct BackupAttachmentDownloadProgressView: View {
|
||||
"BACKUP_SETTINGS_DOWNLOAD_PROGRESS_SUBTITLE_PAUSED_NEEDS_DISK_SPACE",
|
||||
comment: "Subtitle for a progress bar tracking downloads that are paused because they need more disk space available. Embeds {{ the amount of space needed as a file size, e.g. 100 MB }}."
|
||||
),
|
||||
bytesRequired.formatted(.owsByteCount)
|
||||
bytesRequired.formatted(.owsByteCount())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2166,8 +2169,8 @@ private struct BackupAttachmentUploadProgressView: View {
|
||||
"BACKUP_SETTINGS_UPLOAD_PROGRESS_SUBTITLE_RUNNING",
|
||||
comment: "Subtitle for a progress bar tracking active uploading. Embeds 1:{{ the amount uploaded as a file size, e.g. 100 MB }}; 2:{{ the total amount to upload as a file size, e.g. 1 GB }}; 3:{{ the percentage uploaded as a percent, e.g. 40% }}."
|
||||
),
|
||||
bytesUploaded.formatted(.owsByteCount),
|
||||
totalBytesToUpload.formatted(.owsByteCount),
|
||||
bytesUploaded.formatted(.owsByteCount()),
|
||||
totalBytesToUpload.formatted(.owsByteCount()),
|
||||
percentageUploaded.formatted(.percent.precision(.fractionLength(0)))
|
||||
)
|
||||
case .pausedLowBattery:
|
||||
@@ -2493,7 +2496,7 @@ private struct BackupDetailsView: View {
|
||||
comment: "Label for a menu item explaining the size of the user's backup."
|
||||
))
|
||||
Spacer()
|
||||
Text(lastBackupSizeBytes.formatted(.owsByteCount))
|
||||
Text(lastBackupSizeBytes.formatted(.owsByteCount()))
|
||||
.foregroundStyle(Color.Signal.secondaryLabel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +305,10 @@ struct ChooseBackupPlanView: View {
|
||||
"CHOOSE_BACKUP_PLAN_BULLET_STORAGE_AMOUNT",
|
||||
comment: "Text for a bullet point in a list of Backup features, describing the amount of included storage. Embeds {{ the amount of storage preformatted as a localized byte count, e.g. '100 GB' }}."
|
||||
),
|
||||
viewModel.storageAllowanceBytes.formatted(.owsByteCount),
|
||||
viewModel.storageAllowanceBytes.formatted(.owsByteCount(
|
||||
fudgeBase2ToBase10: true,
|
||||
zeroPadFractionDigits: false,
|
||||
)),
|
||||
)),
|
||||
],
|
||||
isCurrentPlan: viewModel.initialPlanSelection == .paid,
|
||||
|
||||
@@ -4,31 +4,114 @@
|
||||
//
|
||||
|
||||
public struct OWSByteCountFormatStyle: FormatStyle {
|
||||
private let style: ByteCountFormatStyle.Style
|
||||
private let fudgeBase2ToBase10: Bool
|
||||
private let zeroPadFractionDigits: Bool
|
||||
|
||||
public init() {
|
||||
self.style = .decimal
|
||||
/// - 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(_ byteCount: UInt64) -> String {
|
||||
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 (the number of digits is controlled by the
|
||||
// formatter: see `isAdaptive`, which defaults to true) to keep a fixed
|
||||
// number of digits. Otherwise, it can look "jumpy" (e.g., going from
|
||||
// 400 MB to 400.1 MB).
|
||||
byteFormatter.zeroPadsFractionDigits = true
|
||||
// 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 var owsByteCount: OWSByteCountFormatStyle {
|
||||
return 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
|
||||
}
|
||||
}
|
||||
|
||||
28
SignalUI/SwiftUIExtensions/OWSByteCountFormatStyleTest.swift
Normal file
28
SignalUI/SwiftUIExtensions/OWSByteCountFormatStyleTest.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Testing
|
||||
|
||||
@testable import SignalUI
|
||||
|
||||
struct OWSByteCountFormatStyleTest {
|
||||
@Test(arguments: [
|
||||
(0, 0),
|
||||
(1024, 1000), // 1 KiB
|
||||
(1_048_576, 1_000_000), // 1 MiB
|
||||
(1_048_577, nil), // 1 MiB + 1 B
|
||||
(1_073_741_824, 1_000_000_000), // 1 GiB
|
||||
(1_074_790_400, nil), // 1 GiB + 2 MiB
|
||||
(39_728_447_488, 37_000_000_000), // 37 GiB
|
||||
(107_374_182_400, 100_000_000_000), // 100 GiB
|
||||
(1_099_511_627_776, 1_000_000_000_000), // 1 TiB
|
||||
(1_125_899_906_842_624, 1_000_000_000_000_000), // 1 PiB
|
||||
])
|
||||
func fudgingBase2ToBase10ByteCount(byteCount: UInt64, expected: UInt64?) {
|
||||
#expect(
|
||||
expected == OWSBase2ByteCountFudger.fudgeBase2ToBase10(byteCount)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user