Implement 'monetary.h', including initial structure and strfmon() function

This commit is contained in:
Guillaume Gielly
2024-12-23 21:32:56 +01:00
parent a4ba9cf0ad
commit 610909bd2b
5 changed files with 475 additions and 15 deletions
Generated
+14 -14
View File
@@ -4,9 +4,9 @@ version = 3
[[package]]
name = "autocfg"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base64ct"
@@ -91,9 +91,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
version = "0.2.14"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [
"libc",
]
@@ -184,9 +184,9 @@ version = "0.1.0"
[[package]]
name = "libc"
version = "0.2.158"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libredox"
@@ -263,9 +263,9 @@ version = "0.1.0"
[[package]]
name = "proc-macro2"
version = "1.0.86"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
@@ -464,9 +464,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.77"
version = "2.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
dependencies = [
"proc-macro2",
"quote",
@@ -481,15 +481,15 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-width"
version = "0.1.13"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "version_check"
+1 -1
View File
@@ -36,7 +36,7 @@ pub mod libgen;
pub mod limits;
pub mod locale;
// math.h implemented in C
// TODO: monetary.h
pub mod monetary;
// TODO: mqueue.h
// TODO: ndbm.h
pub mod net_if;
+12
View File
@@ -0,0 +1,12 @@
sys_includes = ["stddef.h", "stdint.h", "features.h"]
include_guard = "_RELIBC_MONETARY_H"
language = "C"
style = "tag"
no_includes = true
cpp_compat = true
[enum]
prefix_with_name = true
+142
View File
@@ -0,0 +1,142 @@
/// monetary.h implementation for Redox, following the POSIX standard.
/// Following https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/monetary.h.html
///
/// We should provide a strfmon() implementation that formats a monetary value.
/// according to the current locale (TODO).
use alloc::string::{String, ToString};
use core::{ffi::CStr, ptr, slice, str};
extern crate alloc;
mod strfmon;
#[deny(unsafe_op_in_unsafe_fn)]
#[repr(C)]
struct LocaleMonetaryInfo {
int_curr_symbol: &'static str,
currency_symbol: &'static str,
mon_decimal_point: &'static str,
mon_thousands_sep: &'static str,
mon_grouping: &'static [u8],
positive_sign: &'static str,
negative_sign: &'static str,
int_frac_digits: u8,
frac_digits: u8,
p_cs_precedes: bool,
p_sep_by_space: bool,
p_sign_posn: u8,
n_cs_precedes: bool,
n_sep_by_space: bool,
n_sign_posn: u8,
}
const DEFAULT_MONETARY: LocaleMonetaryInfo = LocaleMonetaryInfo {
int_curr_symbol: "USD ",
currency_symbol: "$",
mon_decimal_point: ".",
mon_thousands_sep: ",",
mon_grouping: &[3, 3],
positive_sign: "",
negative_sign: "-",
int_frac_digits: 2,
frac_digits: 2,
p_cs_precedes: true,
p_sep_by_space: false,
p_sign_posn: 1,
n_cs_precedes: true,
n_sep_by_space: false,
n_sign_posn: 1,
};
#[derive(Default)]
struct FormatFlags {
left_justify: bool,
force_sign: bool,
space_sign: bool,
use_parens: bool,
suppress_symbol: bool,
field_width: Option<usize>,
left_precision: Option<usize>,
right_precision: Option<usize>,
international: bool,
}
/// Use our own floating point implementation
/// TODO : use num-traits crate
#[inline]
fn my_fabs(x: f64) -> f64 {
if x < 0.0 {
-x
} else {
x
}
}
#[inline]
fn my_floor(x: f64) -> f64 {
let i = x as i64;
if x < 0.0 && x != i as f64 {
(i - 1) as f64
} else {
i as f64
}
}
#[inline]
fn my_trunc(x: f64) -> f64 {
if x < 0.0 {
-my_floor(-x)
} else {
my_floor(x)
}
}
#[inline]
fn my_round(x: f64) -> f64 {
my_floor(x + 0.5)
}
#[inline]
fn my_pow10(n: usize) -> f64 {
let mut result = 1.0;
for _ in 0..n {
result *= 10.0;
}
result
}
/// TODO : improve grouping implementation
fn apply_grouping(int_str: &str, monetary: &LocaleMonetaryInfo) -> String {
let mut grouped = String::with_capacity(int_str.len() * 2);
let mut count = 0;
let mut group_idx = 0;
// The grouping array can have up to 4 elements, but the last element is always 0
for c in int_str.chars().rev() {
if count > 0 && count % monetary.mon_grouping[group_idx] == 0 {
grouped.push_str(monetary.mon_thousands_sep);
// Move to next grouping size if available
if group_idx + 1 < monetary.mon_grouping.len() {
group_idx += 1;
}
}
grouped.push(c);
count += 1;
}
// Reverse the string to get the correct order
grouped.chars().rev().collect()
}
/// Safe handling of large monetary values. Returns None if the value is too large to format
fn format_value_parts(value: f64, frac_digits: usize) -> Option<(String, i64)> {
let abs_value = my_fabs(value);
if abs_value > (i64::MAX as f64) {
return None;
}
let int_part = my_trunc(abs_value) as i64;
let scale = my_pow10(frac_digits);
let frac_part = my_round((abs_value - int_part as f64) * scale) as i64;
Some((int_part.to_string(), frac_part))
}
+306
View File
@@ -0,0 +1,306 @@
use super::{
my_fabs, my_pow10, my_round, my_trunc, FormatFlags, LocaleMonetaryInfo, DEFAULT_MONETARY,
};
use alloc::string::{String, ToString};
use core::{ffi::CStr, ptr, slice, str};
/*
The strfmon() function formats a monetary value according to the format string format and writes the result to the character
array s of size maxsize.
The format string format is a string that can contain plain characters and format specifiers.
The format specifiers start with the character '%' and are followed by one or more flags, an optional field width,
an optional left precision, an optional right precision, and a conversion specifier.
The conversion specifier can be either 'i' or 'n'. The 'i' specifier formats the monetary value according to the
locale-specific rules, while the 'n' specifier formats the monetary value according to the international rules.
The function returns the number of characters written to the buffer, excluding the null terminator, or -1 if an error occurred.
*/
pub unsafe extern "C" fn strfmon(
s: *mut i8, // char *
maxsize: usize, // size_t
format: *const i8, // const char *
mut args: ...
) -> isize {
if s.is_null() || format.is_null() || maxsize == 0 {
// Check for null pointers and zero size
return -1;
}
let format_str = match unsafe { CStr::from_ptr(format) }.to_str() {
Ok(s) => s, // Convert the format string to a string
Err(_) => return -1, // Return -1 if the format string is not valid
};
// Buffer to write the formatted string
let buffer = unsafe { slice::from_raw_parts_mut(s as *mut u8, maxsize) };
let mut pos = 0;
let mut format_chars = format_str.chars().peekable();
while let Some(c) = format_chars.next() {
if c != '%' {
if pos >= buffer.len() {
return -1;
}
// Write the character to the buffer
buffer[pos] = c as u8; // Write the character to the buffer
pos += 1; // Move to the next position
continue; // Move to the next character
}
// Handle '%%' as an escape sequence
if format_chars.peek() == Some(&'%') {
if pos >= buffer.len() {
return -1; // Buffer overflow
}
buffer[pos] = b'%'; // Write '%' to the buffer
pos += 1; // Move to the next position
format_chars.next(); // Skip the second '%'
continue;
}
let mut flags = FormatFlags::default();
// Parse flags
while let Some(&next_char) = format_chars.peek() {
match next_char {
'=' => flags.left_justify = true,
'+' => flags.force_sign = true,
' ' => flags.space_sign = true,
'(' => flags.use_parens = true,
'!' => flags.suppress_symbol = true,
_ => break,
}
format_chars.next();
}
// Parse field width
let mut num_str = String::new();
while let Some(&c) = format_chars.peek() {
if !c.is_ascii_digit() {
break;
}
num_str.push(c); // Add the character to the number string
format_chars.next(); // Move to the next character
}
if !num_str.is_empty() {
flags.field_width = num_str.parse().ok(); // Parse the number string
}
// Parse left precision
if format_chars.peek() == Some(&'#') {
format_chars.next(); // Skip the '#'
num_str.clear(); // Clear the number string
while let Some(&c) = format_chars.peek() {
if !c.is_ascii_digit() {
// Check if the character is a digit
break;
}
num_str.push(c);
format_chars.next();
}
flags.left_precision = num_str.parse().ok();
}
// Parse right precision
if format_chars.peek() == Some(&'.') {
format_chars.next();
num_str.clear();
while let Some(&c) = format_chars.peek() {
if !c.is_ascii_digit() {
break;
}
num_str.push(c);
format_chars.next();
}
flags.right_precision = num_str.parse().ok();
}
match format_chars.next() {
Some('i') => {
flags.international = true;
let value = unsafe { args.arg::<f64>() };
if let Some(written) =
format_monetary(&mut buffer[pos..], value, &DEFAULT_MONETARY, &flags)
{
pos += written;
} else {
return -1;
}
}
Some('n') => {
let value = unsafe { args.arg::<f64>() }; // Get the next argument as a f64
if let Some(written) =
format_monetary(&mut buffer[pos..], value, &DEFAULT_MONETARY, &flags)
{
pos += written; // Move the position by the number of characters written
} else {
return -1;
}
}
_ => return -1,
}
}
if pos >= buffer.len() {
return -1;
}
buffer[pos] = 0; // Null-terminate the buffer
pos as isize // Return the number of characters written
}
fn format_monetary(
buffer: &mut [u8],
value: f64,
monetary: &LocaleMonetaryInfo,
flags: &FormatFlags,
) -> Option<usize> {
let mut pos = 0; // Initialize the position to 0
let is_negative = value < 0.0; // Check if the value is negative
let abs_value = my_fabs(value); // Get the absolute value of the number
// Check if the international flag is set
let frac_digits = if flags.international {
flags
.right_precision
.unwrap_or(monetary.int_frac_digits as usize)
} else {
flags
.right_precision
.unwrap_or(monetary.frac_digits as usize)
};
let scale = my_pow10(frac_digits);
let mut int_part = my_trunc(abs_value) as i64;
let mut frac_part = my_round((abs_value - int_part as f64) * scale) as i64;
// Ensure that the fractional part (frac_part) doesnt overflow due to rounding
// when abs_value is very close to the next integer value.
if frac_part >= scale as i64 {
// Handle carry-over to the integer part
int_part += 1;
frac_part = 0;
}
let mut int_str = int_part.to_string();
if let Some(left_prec) = flags.left_precision {
if int_str.len() > left_prec {
return None;
}
while int_str.len() < left_prec {
int_str.insert(0, '0');
}
}
// Apply grouping
let mut grouped = String::with_capacity(int_str.len() * 2);
let mut group_idx = 0;
let mut count = 0;
// The grouping array can have up to 4 elements, but the last element is always 0
for c in int_str.chars().rev() {
if count > 0
&& count % monetary.mon_grouping[group_idx.min(monetary.mon_grouping.len() - 1)] == 0
{
grouped.push_str(monetary.mon_thousands_sep);
}
grouped.push(c);
count += 1;
}
// Reverse the string to get the correct order
let mut result = String::with_capacity(grouped.len() + 20);
// Add the sign
let sign_posn = if is_negative {
monetary.n_sign_posn
} else {
monetary.p_sign_posn
};
// Add the sign
let sign = if is_negative {
monetary.negative_sign
} else if flags.force_sign {
monetary.positive_sign
} else if flags.space_sign {
" "
} else {
""
};
// Add the sign
if sign_posn == 0 {
result.push('(');
} else if sign_posn == 1 {
result.push_str(sign);
}
// Add the currency symbol
let cs_precedes = if is_negative {
monetary.n_cs_precedes
} else {
monetary.p_cs_precedes
};
// Add a space between the currency symbol and the value
let sep_by_space = if is_negative {
monetary.n_sep_by_space
} else {
monetary.p_sep_by_space
};
// Add the currency symbol
let symbol = if flags.suppress_symbol {
""
} else if flags.international {
monetary.int_curr_symbol
} else {
monetary.currency_symbol
};
// Add the currency symbol
if cs_precedes {
result.push_str(symbol);
if sep_by_space {
result.push(' ');
}
}
// Add the value
result.push_str(&grouped.chars().rev().collect::<String>());
if frac_digits > 0 {
result.push_str(monetary.mon_decimal_point);
result.push_str(&format!("{:0width$}", frac_part, width = frac_digits));
}
if !cs_precedes {
if sep_by_space {
result.push(' ');
}
result.push_str(symbol);
}
if sign_posn == 0 {
result.push(')');
}
if let Some(width) = flags.field_width {
if result.len() < width {
let padding = " ".repeat(width - result.len());
if flags.left_justify {
result.push_str(&padding);
} else {
result = padding + &result;
}
}
}
// Write the formatted string to the buffer
for (i, &b) in result.as_bytes().iter().enumerate() {
if pos + i >= buffer.len() {
return None;
}
buffer[pos + i] = b;
}
Some(pos + result.len())
}