// Copyright (C) 2024 Huawei Device Co., Ltd.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Locale-aware file size formatting.
//!
//! Converts a byte count into a human-readable string (B/KB/MB/GB) with
//! locale-specific thousand separators, decimal points, and unit labels
//! determined by the system language.
use super::ffi::GetSystemLanguage;
// Language codes that use comma as thousand separator
const COMMA_SEPARATOR_LIST: &[&str] = &["hi", "bn"];
// Language codes that use space as thousand separator
const SPACE_SEPARATOR_LIST: &[&str] = &["fi"];
// Language codes that use dot as thousand separator
const DOT_SEPARATOR_LIST: &[&str] = &["id"];
// Language codes that use comma as decimal point
const COMMA_DECIMAL_POINT_LIST: &[&str] = &["sl", "lt", "fi", "nl", "da", "id"];
// Language codes that require space between number and unit
const SPACE_BEFORE_UNIT_LIST: &[&str] = &[
"sl", "lt", "hi", "fi", "nl", "da", "my", "bn", "zh-Hant", "en",
];
// Language codes that use 'T' instead of 'B' for byte units (e.g., 'KT' instead of 'KB')
const T_UNIT_LIST: &[&str] = &["fi"];
/// Represents a formatted file size with locale-aware components.
#[derive(Debug)]
struct FormattedSize {
/// Integer part of the size value.
integer: u64,
/// Optional decimal part of the size value (used for units larger than bytes).
decimal: Option<String>,
/// String representation of the size unit (B, KB, MB, GB or locale variants).
unit_str: &'static str,
}
/// Size units for file size representation.
#[derive(Debug, PartialEq)]
enum Unit {
/// Byte unit (B).
Bytes,
/// Kilobyte unit (KB) = 1024 bytes.
KiloBytes,
/// Megabyte unit (MB) = 1024 KB.
MegaBytes,
/// Gigabyte unit (GB) = 1024 MB.
GigaBytes,
}
impl Unit {
/// Returns the string representation of the unit based on the specified language.
///
/// # Arguments
///
/// * `lang` - System language code to determine unit format
///
/// # Returns
///
/// String representation of the unit (B/KB/MB/GB or locale-specific variants)
fn as_str(&self, lang: &str) -> &'static str {
if T_UNIT_LIST.contains(&lang) {
match self {
Unit::Bytes => "T",
Unit::KiloBytes => "KT",
Unit::MegaBytes => "MT",
Unit::GigaBytes => "GT",
}
} else {
match self {
Unit::Bytes => "B",
Unit::KiloBytes => "KB",
Unit::MegaBytes => "MB",
Unit::GigaBytes => "GB",
}
}
}
}
impl FormattedSize {
/// Creates a new FormattedSize instance with the given size and unit.
///
/// # Arguments
///
/// * `size` - Size value to format
/// * `unit_str` - Unit to use for the size
/// * `lang` - Language code to determine formatting
///
/// # Returns
///
/// New FormattedSize instance with integer and optional decimal parts
fn format_size_with_unit(size: f64, unit_str: &Unit, lang: &str) -> Self {
// Extract integer part of the size
let integer = size.trunc() as u64;
// Calculate decimal part (only for non-byte units)
let decimal = if unit_str == &Unit::Bytes {
None
} else {
Some(format!("{:02}", (size.fract() * 100.0).floor()))
};
let unit_str = unit_str.as_str(lang);
Self {
integer,
decimal,
unit_str,
}
}
/// Determines the thousand separator to use based on the specified language.
///
/// # Arguments
///
/// * `lang` - System language code to determine separator
///
/// # Returns
///
/// String to use as thousand separator (comma, space, dot, or empty string)
fn separator(&self, lang: &str) -> &'static str {
match true {
_ if COMMA_SEPARATOR_LIST.contains(&lang) => ",",
_ if SPACE_SEPARATOR_LIST.contains(&lang) => " ",
_ if DOT_SEPARATOR_LIST.contains(&lang) => ".",
_ => "",
}
}
/// Determines the decimal point character to use based on the specified language.
///
/// # Arguments
///
/// * `lang` - System language code to determine decimal point
///
/// # Returns
///
/// String to use as decimal point (comma or period), or empty string if no decimal part
fn decimal_point(&self, lang: &str) -> &'static str {
if self.decimal.is_none() {
return "";
}
if COMMA_DECIMAL_POINT_LIST.contains(&lang) {
","
} else {
"."
}
}
/// Determines whether a space is needed between the number and unit based on the language.
///
/// # Arguments
///
/// * `lang` - System language code to determine spacing
///
/// # Returns
///
/// Boolean indicating whether a space is needed before the unit
fn needs_space_before_unit(&self, lang: &str) -> bool {
SPACE_BEFORE_UNIT_LIST.contains(&lang)
}
/// Formats the integer part with thousand separators.
///
/// # Arguments
///
/// * `separator` - Separator to use between thousands places
///
/// # Returns
///
/// String representation of the integer with proper thousand separators
fn integer_format_with_separator(&self, separator: &str) -> String {
let num_str = self.integer.to_string();
let mut result = String::new();
for (i, c) in num_str.chars().rev().enumerate() {
if i != 0 && i % 3 == 0 {
result.push_str(separator);
}
result.push(c);
}
result.chars().rev().collect()
}
/// Formats the size with locale-specific formatting rules.
///
/// # Arguments
///
/// * `lang` - System language code to determine formatting rules
///
/// # Returns
///
/// Fully formatted string representation of the size with proper locale formatting
fn with_locale(&self, lang: &str) -> String {
// Get locale-specific formatting components
let separator = self.separator(lang);
let decimal_point = self.decimal_point(lang);
let space = if self.needs_space_before_unit(lang) {
" "
} else {
""
};
// Format integer with appropriate thousand separators
let integer = self.integer_format_with_separator(separator);
// Combine all components into the final formatted string
format!(
"{}{}{}{}{}",
integer,
decimal_point,
self.decimal.as_deref().unwrap_or(""),
space,
self.unit_str
)
}
}
/// Formats a file size in bytes to a human-readable string using the system language.
///
/// # Arguments
///
/// * `current` - File size in bytes
///
/// # Returns
///
/// Human-readable string representation of the size with appropriate unit and formatting
///
/// # Examples
///
/// ```rust
/// # use service::notification_bar::progress_size::progress_size;
/// assert!(progress_size(512).contains("512 B"));
/// assert!(progress_size(1536).contains("1.50")); // 1.50 KB or similar based on locale
/// assert!(progress_size(1048576).contains("1.00")); // 1.00 MB or similar based on locale
/// ```
pub fn progress_size(current: u64) -> String {
let lang = GetSystemLanguage();
progress_size_with_lang(current, &lang)
}
/// Formats a file size in bytes to a human-readable string using the specified language.
///
/// # Arguments
///
/// * `current` - File size in bytes
/// * `lang` - Language code to use for formatting
///
/// # Returns
///
/// Human-readable string representation of the size with locale-specific formatting
fn progress_size_with_lang(current: u64, lang: &str) -> String {
let (size, unit_str) = calculate_size_and_unit(current);
let formatted = FormattedSize::format_size_with_unit(size, &unit_str, lang);
formatted.with_locale(lang)
}
/// Calculates the appropriate size value and unit for a given byte count.
///
/// # Arguments
///
/// * `current` - File size in bytes
///
/// # Returns
///
/// Tuple containing the adjusted size value and corresponding unit
///
/// # Notes
///
/// This function uses base-1024 units (1 KB = 1024 bytes) as is standard for file sizes.
fn calculate_size_and_unit(current: u64) -> (f64, Unit) {
match current {
0..=1023 => (current as f64, Unit::Bytes),
1024..=1_048_575 => (current as f64 / 1024.0, Unit::KiloBytes),
1_048_576..=1_073_741_823 => (current as f64 / 1_048_576.0, Unit::MegaBytes),
_ => (current as f64 / 1_073_741_824.0, Unit::GigaBytes),
}
}
#[cfg(test)]
mod ut_progress_size {
include!("../../../tests/ut/service/notification_bar/ut_progress_size.rs");
}