mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-19 21:18:25 +02:00
feat(rust) : serveur réécrit en Rust, Node retiré ; Calendrier en tête du menu (#571)
- Serveur Rust (server-rs/, axum + sqlx) : les 148 adresses de l'API, l'interface, les migrations de démarrage et les tâches de fond ; même comportement que Node (banc : 2027 cas, 0 écart hors écarts déclarés USERS-02, RAPPRO-21, R4, R5). - Node retiré : server/, dépendances serveur, image Docker sans Node (51 Mo), docker-compose à un seul service applicatif. - R4 : écritures d'un autre site refusées (Sec-Fetch-Site / Origin). - R5 : « aujourd'hui » et sauvegarde automatique à l'heure de Paris. - R6 : mainteneur dans .github/CODEOWNERS. - Menu latéral : Accueil puis Calendrier en tête. - Plan et état : docs/PLAN-MIGRATION-RUST.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ke2qyS8rgacP3ap5iezDbM
This commit is contained in:
1 parent
6760f43e87
commit
600627c632
146 files changed
+31521
-20331
No files matched your search
@@ -0,0 +1,542 @@
|
||||
// Conversions au comportement de JavaScript, sur lesquelles le code Node s'appuie
|
||||
// (Number(), parseInt(), new Date(), longueur d'une chaîne, toISOString()).
|
||||
// Le serveur Node tourne en UTC (image Docker sans TZ) : l'heure locale est l'UTC.
|
||||
|
||||
use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeZone, Utc};
|
||||
use serde_json::{Number, Value};
|
||||
|
||||
const MAX_DATE_MS: f64 = 8.64e15;
|
||||
|
||||
/// Number(valeur)
|
||||
pub fn to_number(value: &Value) -> f64 {
|
||||
match value {
|
||||
Value::Null => 0.0,
|
||||
Value::Bool(true) => 1.0,
|
||||
Value::Bool(false) => 0.0,
|
||||
Value::Number(number) => number.as_f64().unwrap_or(f64::NAN),
|
||||
Value::String(text) => string_to_number(text),
|
||||
Value::Array(items) => match items.as_slice() {
|
||||
[] => 0.0,
|
||||
[single] if !single.is_array() && !single.is_object() => match single {
|
||||
Value::Null => 0.0,
|
||||
other => to_number(&Value::String(display(other))),
|
||||
},
|
||||
_ => f64::NAN,
|
||||
},
|
||||
Value::Object(_) => f64::NAN,
|
||||
}
|
||||
}
|
||||
|
||||
fn display(value: &Value) -> String {
|
||||
match value {
|
||||
Value::String(text) => text.clone(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn string_to_number(text: &str) -> f64 {
|
||||
let trimmed = text.trim();
|
||||
if trimmed.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let (sign, unsigned) = match trimmed.as_bytes()[0] {
|
||||
b'-' => (-1.0, &trimmed[1..]),
|
||||
b'+' => (1.0, &trimmed[1..]),
|
||||
_ => (1.0, trimmed),
|
||||
};
|
||||
if unsigned == "Infinity" {
|
||||
return sign * f64::INFINITY;
|
||||
}
|
||||
// Préfixes 0x / 0o / 0b : sans signe en JavaScript
|
||||
if sign == 1.0 && trimmed.len() > 2 && !trimmed.starts_with(['+', '-']) {
|
||||
let radix = match &trimmed[..2] {
|
||||
"0x" | "0X" => Some(16),
|
||||
"0o" | "0O" => Some(8),
|
||||
"0b" | "0B" => Some(2),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(radix) = radix {
|
||||
return u64::from_str_radix(&trimmed[2..], radix)
|
||||
.map(|n| n as f64)
|
||||
.unwrap_or(f64::NAN);
|
||||
}
|
||||
}
|
||||
let valid = {
|
||||
let bytes = unsigned.as_bytes();
|
||||
let mut i = 0;
|
||||
let mut digits = 0;
|
||||
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
digits += 1;
|
||||
}
|
||||
if i < bytes.len() && bytes[i] == b'.' {
|
||||
i += 1;
|
||||
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
digits += 1;
|
||||
}
|
||||
}
|
||||
if digits > 0 && i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
|
||||
i += 1;
|
||||
if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
|
||||
i += 1;
|
||||
}
|
||||
let start = i;
|
||||
while i < bytes.len() && bytes[i].is_ascii_digit() {
|
||||
i += 1;
|
||||
}
|
||||
if start == i {
|
||||
return f64::NAN;
|
||||
}
|
||||
}
|
||||
digits > 0 && i == bytes.len()
|
||||
};
|
||||
if !valid {
|
||||
return f64::NAN;
|
||||
}
|
||||
unsigned.parse::<f64>().map(|n| sign * n).unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
/// Valeur « vraie » en JavaScript (`if (x)`, `x || défaut`, `Boolean(x)`) :
|
||||
/// fausse pour null, false, 0, NaN et la chaîne vide
|
||||
pub fn truthy(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::Null => false,
|
||||
Value::Bool(flag) => *flag,
|
||||
Value::Number(number) => number.as_f64().is_some_and(|n| n != 0.0 && !n.is_nan()),
|
||||
Value::String(text) => !text.is_empty(),
|
||||
Value::Array(_) | Value::Object(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// parseInt(texte, 10) : chiffres de tête, None si aucun (NaN)
|
||||
pub fn parse_int(text: &str) -> Option<f64> {
|
||||
let trimmed = text.trim_start();
|
||||
let (sign, rest) = match trimmed.chars().next() {
|
||||
Some('-') => (-1.0, &trimmed[1..]),
|
||||
Some('+') => (1.0, &trimmed[1..]),
|
||||
_ => (1.0, trimmed),
|
||||
};
|
||||
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
if digits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
digits.parse::<f64>().ok().map(|n| sign * n)
|
||||
}
|
||||
|
||||
/// Blancs retirés par String.prototype.trim() (WhiteSpace et LineTerminator) :
|
||||
/// U+FEFF en fait partie, U+0085 non, à l'inverse de str::trim
|
||||
fn is_js_whitespace(c: char) -> bool {
|
||||
matches!(
|
||||
c,
|
||||
'\t' | '\n' | '\u{0B}' | '\u{0C}' | '\r' | ' ' | '\u{A0}' | '\u{1680}' | '\u{2000}'
|
||||
..='\u{200A}' | '\u{2028}' | '\u{2029}' | '\u{202F}' | '\u{205F}' | '\u{3000}' | '\u{FEFF}'
|
||||
)
|
||||
}
|
||||
|
||||
/// texte.trim()
|
||||
pub fn trim(text: &str) -> &str {
|
||||
text.trim_matches(is_js_whitespace)
|
||||
}
|
||||
|
||||
/// String(valeur) : texte tel quel, nombre sans « .0 », tableau joint par « , »,
|
||||
/// objet « [object Object] » (gabarits `${…}`, clés d'objet)
|
||||
pub fn to_js_string(value: &Value) -> String {
|
||||
string_of(value)
|
||||
}
|
||||
|
||||
/// parseFloat(texte) : plus long préfixe décimal après les blancs de tête, NaN sinon
|
||||
pub fn parse_float(text: &str) -> f64 {
|
||||
let text = text.trim_start_matches(is_js_whitespace);
|
||||
let (sign, unsigned) = match text.as_bytes().first() {
|
||||
Some(b'-') => (-1.0, &text[1..]),
|
||||
Some(b'+') => (1.0, &text[1..]),
|
||||
_ => (1.0, text),
|
||||
};
|
||||
if unsigned.starts_with("Infinity") {
|
||||
return sign * f64::INFINITY;
|
||||
}
|
||||
let bytes = unsigned.as_bytes();
|
||||
let mut end = 0;
|
||||
let mut digits = 0;
|
||||
while end < bytes.len() && bytes[end].is_ascii_digit() {
|
||||
end += 1;
|
||||
digits += 1;
|
||||
}
|
||||
if end < bytes.len() && bytes[end] == b'.' {
|
||||
let mut fraction_end = end + 1;
|
||||
while fraction_end < bytes.len() && bytes[fraction_end].is_ascii_digit() {
|
||||
fraction_end += 1;
|
||||
digits += 1;
|
||||
}
|
||||
end = fraction_end;
|
||||
}
|
||||
if digits == 0 {
|
||||
return f64::NAN;
|
||||
}
|
||||
// Exposant seulement s'il a au moins un chiffre (« 1e » vaut 1)
|
||||
if end < bytes.len() && matches!(bytes[end], b'e' | b'E') {
|
||||
let mut exponent_end = end + 1;
|
||||
if exponent_end < bytes.len() && matches!(bytes[exponent_end], b'+' | b'-') {
|
||||
exponent_end += 1;
|
||||
}
|
||||
let start = exponent_end;
|
||||
while exponent_end < bytes.len() && bytes[exponent_end].is_ascii_digit() {
|
||||
exponent_end += 1;
|
||||
}
|
||||
if exponent_end > start {
|
||||
end = exponent_end;
|
||||
}
|
||||
}
|
||||
unsigned[..end]
|
||||
.parse::<f64>()
|
||||
.map(|n| sign * n)
|
||||
.unwrap_or(f64::NAN)
|
||||
}
|
||||
|
||||
/// encodeURIComponent(texte) : tout sauf A-Z a-z 0-9 - _ . ! ~ * ' ( ) en %XX (UTF-8)
|
||||
pub fn encode_uri_component(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
for byte in text.bytes() {
|
||||
if byte.is_ascii_alphanumeric() || b"-_.!~*'()".contains(&byte) {
|
||||
out.push(byte as char);
|
||||
} else {
|
||||
out.push_str(&format!("%{byte:02X}"));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Buffer.from(texte, 'base64') : alphabets standard et « url » acceptés, autres
|
||||
/// caractères ignorés, lecture arrêtée au premier « = »
|
||||
pub fn buffer_from_base64(text: &str) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(text.len() * 3 / 4);
|
||||
let (mut bits, mut count) = (0u32, 0u32);
|
||||
for c in text.chars() {
|
||||
let value = match c {
|
||||
'A'..='Z' => c as u32 - 'A' as u32,
|
||||
'a'..='z' => c as u32 - 'a' as u32 + 26,
|
||||
'0'..='9' => c as u32 - '0' as u32 + 52,
|
||||
'+' | '-' => 62,
|
||||
'/' | '_' => 63,
|
||||
'=' => break,
|
||||
_ => continue,
|
||||
};
|
||||
bits = (bits << 6) | value;
|
||||
count += 6;
|
||||
if count >= 8 {
|
||||
count -= 8;
|
||||
out.push((bits >> count) as u8);
|
||||
bits &= (1 << count) - 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Nombre JSON tel que JSON.stringify l'écrirait (entier sans « .0 »)
|
||||
pub fn number_value(number: f64) -> Value {
|
||||
if number.fract() == 0.0 && number.abs() <= 9_007_199_254_740_991.0 {
|
||||
Value::Number(Number::from(number as i64))
|
||||
} else {
|
||||
Number::from_f64(number).map(Value::Number).unwrap_or(Value::Null)
|
||||
}
|
||||
}
|
||||
|
||||
/// String(nombre) pour les messages
|
||||
pub fn number_to_string(number: f64) -> String {
|
||||
number_string(number)
|
||||
}
|
||||
|
||||
/// String(nombre) complet (Number.prototype.toString) : -0 → « 0 », notation
|
||||
/// exponentielle au-delà de 1e21 et en deçà de 1e-6, NaN et Infinity
|
||||
pub fn number_string(number: f64) -> String {
|
||||
if number.is_nan() {
|
||||
return "NaN".into();
|
||||
}
|
||||
if number.is_infinite() {
|
||||
return if number > 0.0 { "Infinity" } else { "-Infinity" }.into();
|
||||
}
|
||||
if number == 0.0 {
|
||||
return "0".into();
|
||||
}
|
||||
// Chiffres significatifs les plus courts et exposant, comme JavaScript
|
||||
let scientific = format!("{:e}", number.abs());
|
||||
let (mantissa, exponent) = scientific.split_once('e').unwrap_or((&scientific, "0"));
|
||||
let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
|
||||
let k = digits.len() as i32;
|
||||
let n = exponent.parse::<i32>().unwrap_or(0) + 1;
|
||||
let body = if k <= n && n <= 21 {
|
||||
format!("{digits}{}", "0".repeat((n - k) as usize))
|
||||
} else if 0 < n && n <= 21 {
|
||||
format!("{}.{}", &digits[..n as usize], &digits[n as usize..])
|
||||
} else if -6 < n && n <= 0 {
|
||||
format!("0.{}{digits}", "0".repeat((-n) as usize))
|
||||
} else {
|
||||
let sign = if n > 0 { '+' } else { '-' };
|
||||
let (first, rest) = digits.split_at(1);
|
||||
let fraction = if rest.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(".{rest}")
|
||||
};
|
||||
format!("{first}{fraction}e{sign}{}", (n - 1).abs())
|
||||
};
|
||||
if number < 0.0 { format!("-{body}") } else { body }
|
||||
}
|
||||
|
||||
/// String(valeur) d'une valeur JSON (gabarits `${…}`, Array.prototype.join)
|
||||
pub fn string_of(value: &Value) -> String {
|
||||
match value {
|
||||
Value::Null => "null".into(),
|
||||
Value::Bool(flag) => flag.to_string(),
|
||||
Value::Number(number) => number_string(number.as_f64().unwrap_or(f64::NAN)),
|
||||
Value::String(text) => text.clone(),
|
||||
// [a, null, b].join(',') : null vide
|
||||
Value::Array(items) => items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
if item.is_null() {
|
||||
String::new()
|
||||
} else {
|
||||
string_of(item)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
Value::Object(_) => "[object Object]".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Longueur d'une chaîne en JavaScript (unités UTF-16)
|
||||
pub fn length(text: &str) -> usize {
|
||||
text.encode_utf16().count()
|
||||
}
|
||||
|
||||
/// new Date(valeur), None si « Invalid Date »
|
||||
pub fn new_date(value: &Value) -> Option<DateTime<Utc>> {
|
||||
let millis = match value {
|
||||
Value::Null => 0.0,
|
||||
Value::Bool(flag) => f64::from(u8::from(*flag)),
|
||||
Value::Number(number) => number.as_f64()?,
|
||||
Value::String(text) => return parse_date(text),
|
||||
_ => return None,
|
||||
};
|
||||
from_millis(millis)
|
||||
}
|
||||
|
||||
fn from_millis(millis: f64) -> Option<DateTime<Utc>> {
|
||||
if !millis.is_finite() || millis.abs() > MAX_DATE_MS {
|
||||
return None;
|
||||
}
|
||||
Utc.timestamp_millis_opt(millis.trunc() as i64).single()
|
||||
}
|
||||
|
||||
/// Date.parse pour les formats envoyés par l'interface : ISO 8601 (date seule = UTC
|
||||
/// minuit, date et heure sans fuseau = heure locale, donc UTC ici), et « AAAA-MM-JJ HH:MM:SS »
|
||||
pub fn parse_date(text: &str) -> Option<DateTime<Utc>> {
|
||||
let text = text.trim();
|
||||
if let Ok(date) = DateTime::parse_from_rfc3339(text) {
|
||||
return Some(date.with_timezone(&Utc));
|
||||
}
|
||||
if let Ok(date) = NaiveDate::parse_from_str(text, "%Y-%m-%d") {
|
||||
return Some(Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0)?));
|
||||
}
|
||||
for format in [
|
||||
"%Y-%m-%dT%H:%M:%S%.f",
|
||||
"%Y-%m-%dT%H:%M",
|
||||
"%Y-%m-%d %H:%M:%S%.f",
|
||||
"%Y-%m-%d %H:%M",
|
||||
] {
|
||||
if let Ok(naive) = NaiveDateTime::parse_from_str(text, format) {
|
||||
return Some(Utc.from_utc_datetime(&naive));
|
||||
}
|
||||
}
|
||||
for format in [
|
||||
"%Y-%m-%dT%H:%M:%S%.f%:z",
|
||||
"%Y-%m-%dT%H:%M%:z",
|
||||
"%Y-%m-%dT%H:%M:%S%.fZ",
|
||||
"%Y-%m-%dT%H:%MZ",
|
||||
] {
|
||||
if let Ok(date) = DateTime::parse_from_str(text, format) {
|
||||
return Some(date.with_timezone(&Utc));
|
||||
}
|
||||
if let Ok(naive) = NaiveDateTime::parse_from_str(text, format) {
|
||||
return Some(Utc.from_utc_datetime(&naive));
|
||||
}
|
||||
}
|
||||
if let Ok(date) = NaiveDate::parse_from_str(text, "%Y-%m") {
|
||||
return Some(Utc.from_utc_datetime(&date.and_hms_opt(0, 0, 0)?));
|
||||
}
|
||||
DateTime::parse_from_rfc2822(text)
|
||||
.ok()
|
||||
.map(|date| date.with_timezone(&Utc))
|
||||
}
|
||||
|
||||
/// date.toISOString()
|
||||
pub fn iso(date: &DateTime<Utc>) -> String {
|
||||
date.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn number_comme_javascript() {
|
||||
assert_eq!(to_number(&json!("")), 0.0);
|
||||
assert_eq!(to_number(&json!(" 12 ")), 12.0);
|
||||
assert_eq!(to_number(&json!("1e3")), 1000.0);
|
||||
assert_eq!(to_number(&json!("0x1A")), 26.0);
|
||||
assert!(to_number(&json!("12abc")).is_nan());
|
||||
assert!(to_number(&json!("abc")).is_nan());
|
||||
assert_eq!(to_number(&json!(null)), 0.0);
|
||||
assert_eq!(to_number(&json!(true)), 1.0);
|
||||
assert_eq!(to_number(&json!([])), 0.0);
|
||||
assert_eq!(to_number(&json!(["7"])), 7.0);
|
||||
assert!(to_number(&json!({})).is_nan());
|
||||
assert_eq!(to_number(&json!("-3.5")), -3.5);
|
||||
assert_eq!(to_number(&json!(".5")), 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verite_comme_javascript() {
|
||||
for falsy in [json!(null), json!(false), json!(0), json!(0.0), json!("")] {
|
||||
assert!(!truthy(&falsy), "{falsy}");
|
||||
}
|
||||
for truthy_value in [json!(true), json!(-1), json!("0"), json!([]), json!({})] {
|
||||
assert!(truthy(&truthy_value), "{truthy_value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_int_comme_javascript() {
|
||||
assert_eq!(parse_int("12abc"), Some(12.0));
|
||||
assert_eq!(parse_int(" -4"), Some(-4.0));
|
||||
assert_eq!(parse_int("abc"), None);
|
||||
assert_eq!(parse_int(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dates_comme_javascript() {
|
||||
assert_eq!(
|
||||
iso(&new_date(&json!("2026-10-15")).unwrap()),
|
||||
"2026-10-15T00:00:00.000Z"
|
||||
);
|
||||
assert_eq!(
|
||||
iso(&new_date(&json!("2026-10-15T08:30")).unwrap()),
|
||||
"2026-10-15T08:30:00.000Z"
|
||||
);
|
||||
assert_eq!(
|
||||
iso(&new_date(&json!("2026-10-15T08:30:00.123+02:00")).unwrap()),
|
||||
"2026-10-15T06:30:00.123Z"
|
||||
);
|
||||
assert_eq!(iso(&new_date(&json!(null)).unwrap()), "1970-01-01T00:00:00.000Z");
|
||||
assert!(new_date(&json!("pas une date")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_de_nombres_comme_javascript() {
|
||||
// Valeurs de String(n) dans Node
|
||||
for (number, expected) in [
|
||||
(0.0, "0"),
|
||||
(-0.0, "0"),
|
||||
(2026.0, "2026"),
|
||||
(-3.0, "-3"),
|
||||
(18.3, "18.3"),
|
||||
(0.1 + 0.2, "0.30000000000000004"),
|
||||
(1.2291666666666667, "1.2291666666666667"),
|
||||
(123456789012345680000.0, "123456789012345680000"),
|
||||
(1e21, "1e+21"),
|
||||
(1.5e300, "1.5e+300"),
|
||||
(0.000001, "0.000001"),
|
||||
(1e-7, "1e-7"),
|
||||
(-2.5e-8, "-2.5e-8"),
|
||||
(f64::NAN, "NaN"),
|
||||
(f64::NEG_INFINITY, "-Infinity"),
|
||||
] {
|
||||
assert_eq!(number_string(number), expected, "{number}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_et_verite_des_valeurs_json() {
|
||||
assert_eq!(string_of(&json!([1, null, "a", [2, 3]])), "1,,a,2,3");
|
||||
assert_eq!(string_of(&json!({ "a": 1 })), "[object Object]");
|
||||
assert_eq!(string_of(&json!(null)), "null");
|
||||
assert_eq!(string_of(&json!(48.5)), "48.5");
|
||||
assert!(!truthy(&json!(0)) && !truthy(&json!("")) && !truthy(&json!(null)));
|
||||
assert!(truthy(&json!("0")) && truthy(&json!([])) && truthy(&json!({})));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_float_comme_javascript() {
|
||||
assert_eq!(parse_float("18.3"), 18.3);
|
||||
assert_eq!(parse_float(" -2.50abc"), -2.5);
|
||||
assert_eq!(parse_float(".5"), 0.5);
|
||||
assert_eq!(parse_float("5."), 5.0);
|
||||
assert_eq!(parse_float("1e3x"), 1000.0);
|
||||
assert_eq!(parse_float("1e"), 1.0);
|
||||
assert_eq!(parse_float("-Infinityx"), f64::NEG_INFINITY);
|
||||
assert!(parse_float("abc").is_nan());
|
||||
assert!(parse_float(".").is_nan());
|
||||
assert!(parse_float("").is_nan());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_float_valeurs_de_node() {
|
||||
// Valeurs relevées avec parseFloat() dans Node 22
|
||||
assert_eq!(parse_float("1250.5"), 1250.5);
|
||||
assert_eq!(parse_float(" 12.5abc"), 12.5);
|
||||
assert_eq!(parse_float("1 234,56"), 1.0);
|
||||
assert!(parse_float("abc").is_nan());
|
||||
assert!(parse_float("").is_nan());
|
||||
assert_eq!(parse_float(".5"), 0.5);
|
||||
assert_eq!(parse_float("-Infinity x"), f64::NEG_INFINITY);
|
||||
assert_eq!(parse_float("1e3"), 1000.0);
|
||||
assert_eq!(parse_float("0x10"), 0.0);
|
||||
assert_eq!(parse_float("1e"), 1.0);
|
||||
assert_eq!(parse_float("+3."), 3.0);
|
||||
assert_eq!(parse_float("1.2.3"), 1.2);
|
||||
assert_eq!(parse_float("\u{a0} 7"), 7.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chaines_comme_javascript() {
|
||||
assert_eq!(trim(" \u{a0}\u{feff}x\u{85}"), "x\u{85}");
|
||||
assert_eq!(to_js_string(&json!(1.0)), "1");
|
||||
assert_eq!(to_js_string(&json!([1, null, "a"])), "1,,a");
|
||||
assert_eq!(to_js_string(&json!({})), "[object Object]");
|
||||
assert_eq!(
|
||||
encode_uri_component("FAC 2001/é?&=#+()!*'~-_."),
|
||||
"FAC%202001%2F%C3%A9%3F%26%3D%23%2B()!*'~-_."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_comme_buffer_from() {
|
||||
// Buffer.from(texte, 'base64').toString('hex') dans Node 22
|
||||
let hex = |text: &str| {
|
||||
buffer_from_base64(text)
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect::<String>()
|
||||
};
|
||||
assert_eq!(hex("ab!cd"), "69b71d");
|
||||
assert_eq!(hex("QU JD"), "414243");
|
||||
assert_eq!(hex("Q=UJD"), "");
|
||||
assert_eq!(hex("QUJ=D"), "4142");
|
||||
assert_eq!(hex("QU-_"), "414fbf");
|
||||
assert_eq!(hex("QUJDRA==x"), "41424344");
|
||||
assert_eq!(hex("Q\u{e9}UJD"), "414243");
|
||||
assert_eq!(buffer_from_base64("JVBE Ri0x\nLjQ=").len(), 8);
|
||||
assert_eq!(buffer_from_base64("a").len(), 0);
|
||||
assert_eq!(buffer_from_base64("AA=A").len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn longueur_utf16() {
|
||||
assert_eq!(length("été"), 3);
|
||||
assert_eq!(length("😀"), 2);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user