Files
CollectFlow/server/src/qlik/date_range.rs
T
Claude 02d9a776f5 Qlik : extraction réseau coupée à 30 s par chromiumoxide, corrigée
Depuis le passage au serveur Rust, la mise à jour réseau d'un fournisseur
(ex. Sovacom) échouait sur « Request timed out » : chromiumoxide coupe toute
commande de page au bout de 30 s, en dur (REQUEST_TIMEOUT de CommandFuture),
sans tenir compte du request_timeout du navigateur. Or toute l'extraction
Qlik tient dans un seul Runtime.evaluate de plusieurs minutes. Playwright,
côté TS, n'avait pas cette limite.

PageQlik::evaluer lance désormais le script sans l'attendre, puis relève son
résultat par des évaluations courtes (20 ms → 1 s). Aucune commande CDP
n'approche plus les 30 s, quelle que soit la durée de l'extraction ; la
recherche produit, qui passe par la même fonction, en profite aussi.

Tests :
- tests/faux-qlik : faux Qlik Sense (HTTPS, NTLM → ticket → cookie, page
  avec jeton CSRF, mini-moteur Engine) ;
- extraction_faux_qlik (ignoré par défaut) : extraction complète de bout en
  bout avec un vrai Chromium, y compris contre celui de l'image de prod ;
- fenetre_identique_au_ts : fenêtre 12 mois identique à l'ancien TS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTseDg4hw2m9tGUJsqPt57
2026-10-14 15:58:40 +00:00

481 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Helper de fenêtre temporelle pour Qlik réseau (port de `src/lib/qlik-date-range.ts`).
//!
//! Aligne la timeline d'extraction Qlik réseau sur la timeline de la grille :
//! - `months_back` mois complets glissants (défaut : 12)
//! - exclut systématiquement le mois courant
//!
//! Référence utilisée : `buildLast12MonthsRange()` de `api-ff-client.ts`
//! (même source que la grille). Si la source officielle change, mettre à jour
//! ici aussi.
//!
//! Qlik référence les dates par un sérial numérique (jours écoulés depuis
//! l'epoch Qlik 1899-12-30). Repères validés :
//! 2025-01-01 → 45658
//! 2025-06-19 → 45827
//! 2026-01-01 → 46023
//! 2026-06-19 → 46192
//!
//! Le filtre côté Qlik s'applique via une sélection de champ `Date` :
//! - valeurs numériques = serials Qlik
//! - la sélection par défaut (`{<Date={...}>}`) est héritée par toutes
//! les master measures qui référencent le champ `Date`.
//!
//! Pourquoi `SelectValues` sur le champ plutôt que set analysis dans `qDef` ?
//! - les master items sont référencés par `qLibraryId` (réutilisation sans
//! devoir dupliquer l'expression)
//! - `qLibraryId` + `qDef` ensemble n'est pas supporté par le moteur QIX
//! - sélectionner toutes les dates de la fenêtre via `SelectValues` revient
//! fonctionnellement à `{<Date={">=…<=…"}>}` pour les mesures qui touchent
//! ce champ, sans dépendre de l'expression interne des master items
//!
//! Pas de secrets ici. Fonctions pures, testées par `cargo test`.
use chrono::{Datelike, Duration, NaiveDate};
use serde::{Deserialize, Serialize};
/// Bornes acceptées pour `months_back`. On reste sur 1..12 pour ne pas casser
/// la sémantique "12 mois glissants alignés sur la grille" : une valeur plus
/// grande n'apporterait pas de signal réseau supplémentaire (la grille
/// n'expose elle-même que ~12 mois). En-deçà de 1 mois, la fenêtre serait
/// vide (mois courant exclu) ou incohérente.
pub const QLIK_MONTHS_BACK_MIN: i64 = 1;
pub const QLIK_MONTHS_BACK_MAX: i64 = 12;
pub const QLIK_MONTHS_BACK_DEFAULT: i64 = 12;
/// Conversion d'une chaîne en nombre avec la sémantique de `Number(s)` en JS :
/// espaces ignorés, chaîne vide → 0, préfixes `0x` / `0o` / `0b`, sinon NaN.
pub fn js_number(raw: &str) -> f64 {
let s = raw.trim();
if s.is_empty() {
return 0.0;
}
let lower = s.to_ascii_lowercase();
for (prefixe, base) in [("0x", 16), ("0o", 8), ("0b", 2)] {
if let Some(reste) = lower.strip_prefix(prefixe) {
return u64::from_str_radix(reste, base).map(|v| v as f64).unwrap_or(f64::NAN);
}
}
match s {
"Infinity" | "+Infinity" => return f64::INFINITY,
"-Infinity" => return f64::NEG_INFINITY,
_ => {}
}
// Rust accepte « inf », « nan »… que JS refuse : on les écarte.
if lower.contains("inf") || lower.contains("nan") {
return f64::NAN;
}
s.parse::<f64>().unwrap_or(f64::NAN)
}
/// Lit la variable d'environnement `name` et renvoie un entier borné entre
/// [`QLIK_MONTHS_BACK_MIN`] et [`QLIK_MONTHS_BACK_MAX`]. Renvoie `default_value`
/// si la variable est absente / vide / non numérique.
pub fn env_months_back(name: &str, default_value: i64) -> i64 {
months_back_from(std::env::var(name).ok().as_deref(), default_value)
}
/// Cœur testable de [`env_months_back`].
pub fn months_back_from(raw: Option<&str>, default_value: i64) -> i64 {
let Some(raw) = raw else { return default_value };
if raw.trim().is_empty() {
return default_value;
}
let parsed = js_number(raw);
if !parsed.is_finite() {
return default_value;
}
(parsed.trunc() as i64).clamp(QLIK_MONTHS_BACK_MIN, QLIK_MONTHS_BACK_MAX)
}
/// Epoch Qlik = 1899-12-30 (équivalent Excel/Lotus 1-2-3).
fn qlik_epoch() -> NaiveDate {
NaiveDate::from_ymd_opt(1899, 12, 30).expect("epoch Qlik")
}
/// Repères de validation.
pub const QLIK_DATE_ANCHORS: [(&str, i64); 4] = [("2025-01-01", 45658), ("2025-06-19", 45827), ("2026-01-01", 46023), ("2026-06-19", 46192)];
fn parse_iso(iso_date: &str) -> Option<(i32, u32, u32)> {
let b = iso_date.as_bytes();
if b.len() != 10 || b[4] != b'-' || b[7] != b'-' {
return None;
}
let chiffres = |r: std::ops::Range<usize>| -> Option<u32> {
let part = &iso_date[r];
if part.bytes().all(|c| c.is_ascii_digit()) {
part.parse().ok()
} else {
None
}
};
Some((chiffres(0..4)? as i32, chiffres(5..7)?, chiffres(8..10)?))
}
/// Jours depuis l'epoch Qlik pour une date (y, m, d), avec le débordement de
/// `Date.UTC` (mois 13 → janvier suivant, jour 32 → mois suivant…).
fn serial_ymd(y: i32, mo: u32, d: u32) -> i64 {
let base = NaiveDate::from_ymd_opt(y, 1, 1).expect("année valide");
let premier_du_mois = base.checked_add_months(chrono::Months::new(mo.saturating_sub(1))).unwrap_or(base);
let date = premier_du_mois + Duration::days(d as i64 - 1);
// Date.UTC(y, -1, …) n'arrive pas ici : le format impose deux chiffres ≥ 00.
let date = if mo == 0 { date - chrono::Months::new(1) } else { date };
(date - qlik_epoch()).num_days()
}
/// Convertit une date ISO `YYYY-MM-DD` en sérial Qlik numérique.
/// Inverse documenté : Qlik epoch = 1899-12-30 (jour 0).
///
/// Erreur si l'entrée n'est pas au format `YYYY-MM-DD`.
pub fn iso_date_to_qlik_serial(iso_date: &str) -> anyhow::Result<i64> {
let Some((y, mo, d)) = parse_iso(iso_date) else {
anyhow::bail!("isoDateToQlikSerial: format attendu YYYY-MM-DD, reçu \"{iso_date}\"");
};
Ok(serial_ymd(y, mo, d))
}
/// Convertit un sérial Qlik en date ISO `YYYY-MM-DD` (utile pour tests/diagnostics).
pub fn qlik_serial_to_iso_date(serial: i64) -> String {
(qlik_epoch() + Duration::days(serial)).format("%Y-%m-%d").to_string()
}
/// Sérial Qlik → entier `AAAAMMJJ` (format de `Date_Key` sur certains modèles).
pub fn qlik_serial_to_ymd(serial: i64) -> i64 {
let d = qlik_epoch() + Duration::days(serial);
d.year() as i64 * 10000 + d.month() as i64 * 100 + d.day() as i64
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QlikDateFilter {
/// Date ISO du 1er jour de la fenêtre (inclus).
pub date_debut: String,
/// Date ISO du dernier jour de la fenêtre (inclus).
pub date_fin: String,
/// Sérial Qlik du 1er jour (= `Date >= …`).
pub q_start: i64,
/// Sérial Qlik du dernier jour (= `Date <= …`).
pub q_end: i64,
/// Libellé humain `YYYY-MM_YYYY-MM` pour logs / cache.
pub label: String,
/// Liste exhaustive des serials journaliers `[qStart..qEnd]` pour SelectValues.
pub daily_serials: Vec<i64>,
/// Expression set analysis prête à intégrer dans un `qDef` de mesure.
pub set_analysis: String,
}
/// Dernier jour du mois (m: 1..12).
fn last_day_of_month(y: i32, m: u32) -> u32 {
let (ny, nm) = if m == 12 { (y + 1, 1) } else { (y, m + 1) };
NaiveDate::from_ymd_opt(ny, nm, 1).expect("mois valide").pred_opt().expect("veille").day()
}
/// Calcule la fenêtre `months_back` mois complets (excluant le mois courant).
///
/// `now` est la date LOCALE du jour (`chrono::Local::now().date_naive()`) : les
/// bornes sont des dates calendaires Qlik, jamais converties en UTC — minuit
/// local en Europe/Paris devenait la veille en UTC et transformait par exemple
/// 2025-07-01 en 2025-06-30, soit une extraction de 13 mois.
///
/// Bornes : `months_back` est ramené dans `[QLIK_MONTHS_BACK_MIN, QLIK_MONTHS_BACK_MAX]`
/// (1..12) sans erreur — on veut que la sync reste possible même si l'env est
/// mal configuré.
pub fn build_grid_network_qlik_date_filter(now: NaiveDate, months_back: i64) -> QlikDateFilter {
let months = months_back.clamp(QLIK_MONTHS_BACK_MIN, QLIK_MONTHS_BACK_MAX) as u32;
let premier_du_mois = NaiveDate::from_ymd_opt(now.year(), now.month(), 1).expect("date valide");
// Fin : dernier jour du mois précédent le mois courant
let end_month = premier_du_mois.pred_opt().expect("veille");
// Début : `months` mois avant le mois courant (1er du mois)
let start_month = premier_du_mois - chrono::Months::new(months);
let date_debut = start_month.format("%Y-%m-%d").to_string();
let date_fin = end_month.format("%Y-%m-%d").to_string();
let q_start = (start_month - qlik_epoch()).num_days();
let q_end = (end_month - qlik_epoch()).num_days();
// qEnd < qStart est impossible ici (au moins un mois complet) : le TS levait
// une exception pour ce cas, inatteignable avec des bornes ramenées à 1..12.
let daily_serials: Vec<i64> = (q_start..=q_end).collect();
let label = format!("{}_{}", &date_debut[..7], &date_fin[..7]);
let set_analysis = format!("Date={{\">={q_start}<={q_end}\"}}");
QlikDateFilter { date_debut, date_fin, q_start, q_end, label, daily_serials, set_analysis }
}
/// Fenêtre du jour (date locale) — `buildGridNetworkQlikDateFilter()` sans argument.
pub fn build_grid_network_qlik_date_filter_now(months_back: i64) -> QlikDateFilter {
build_grid_network_qlik_date_filter(chrono::Local::now().date_naive(), months_back)
}
/// Découpe les serials en lots pour appels `SelectValues` paginés.
/// Par défaut 500 valeurs/appel (largement < limite Qlik ~10k par requête).
pub fn chunk_serials(serials: &[i64], batch_size: usize) -> Vec<Vec<i64>> {
serials.chunks(batch_size.max(1)).map(|c| c.to_vec()).collect()
}
/// Découpe une fenêtre temporelle en sous-fenêtres mensuelles calendaires
/// **inclusives et continues**.
///
/// But : éviter qu'un seul hypercube Qlik ne doive charger 365 jours × N
/// codes d'un coup (provoque `code:15 Request aborted`). Avec cette découpe,
/// chaque mois ≈ 28–31 jours × N codes → beaucoup plus léger pour l'Engine.
///
/// Règles :
/// - Le 1er mois commence à `filter.date_debut` (peut être un jour quelconque).
/// - Chaque mois intermédiaire est un mois calendaire **complet**.
/// - Le dernier mois se termine à `filter.date_fin` (tronqué si pas fin de mois).
/// - Les mois sont contigus : fin d'un mois + 1 = début du suivant.
/// - Si début et fin tombent dans le même mois, on renvoie un seul mois.
///
/// Chaque mois retourné est un `QlikDateFilter` autonome (`label` au format `YYYY-MM`).
pub fn get_month_ranges(filter: &QlikDateFilter) -> Vec<QlikDateFilter> {
let start = filter.date_debut.as_str();
let end = filter.date_fin.as_str();
let num = |s: &str, r: std::ops::Range<usize>| -> i64 { s.get(r).and_then(|x| x.parse().ok()).unwrap_or(0) };
let sy = num(start, 0..4) as i32;
let sm = num(start, 5..7) as u32;
let ey = num(end, 0..4) as i32;
let em = num(end, 5..7) as u32;
let ed = num(end, 8..10) as u32;
let mut out = Vec::new();
let (mut cur_y, mut cur_m) = (sy, sm);
// Sécurité anti-boucle infinie (au cas où) : 100 ans, large.
const MAX_MONTHS: usize = 1200;
let mut safety = 0;
while safety < MAX_MONTHS && (1..=12).contains(&cur_m) {
safety += 1;
let is_first = cur_y == sy && cur_m == sm;
let is_last = cur_y == ey && cur_m == em;
// Début : si c'est le premier mois, on respecte `date_debut` ; sinon le 1er.
let start_day = if is_first { num(start, 8..10) as u32 } else { 1 };
let month_start = format!("{cur_y}-{cur_m:02}-{start_day:02}");
// Fin : si c'est le dernier mois, on respecte `date_fin` ; sinon le dernier jour.
let end_day = if is_last { ed } else { last_day_of_month(cur_y, cur_m) };
let month_end = format!("{cur_y}-{cur_m:02}-{end_day:02}");
let q_start = serial_ymd(cur_y, cur_m, start_day);
let q_end = serial_ymd(cur_y, cur_m, end_day);
let daily_serials: Vec<i64> = (q_start..=q_end).collect();
out.push(QlikDateFilter {
date_debut: month_start,
date_fin: month_end,
q_start,
q_end,
label: format!("{cur_y}-{cur_m:02}"),
daily_serials,
set_analysis: format!("Date={{\">={q_start}<={q_end}\"}}"),
});
if is_last {
break;
}
// Avance d'un mois calendaire (gère le passage décembre → janvier).
cur_m += 1;
if cur_m > 12 {
cur_m = 1;
cur_y += 1;
}
}
if out.is_empty() {
// Fenêtre vide : on retourne un seul mois vide aligné sur date_debut.
out.push(QlikDateFilter {
date_debut: filter.date_debut.clone(),
date_fin: filter.date_fin.clone(),
q_start: filter.q_start,
q_end: filter.q_end,
label: start.get(0..7).unwrap_or(start).to_string(),
daily_serials: Vec::new(),
set_analysis: format!("Date={{\">={}<={}\"}}", filter.q_start, filter.q_end),
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn d(s: &str) -> NaiveDate {
NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
}
/// Valeurs de référence calculées par l'ancien `qlik-date-range.ts`
/// (`buildGridNetworkQlikDateFilter` + `getMonthRanges`).
#[test]
fn fenetre_identique_au_ts() {
for (jour, label, q_start, q_end, premier_mois) in [
("2026-10-10", "2025-10_2026-09", 45931, 46295, (45931, 45961)),
("2026-10-14", "2025-10_2026-09", 45931, 46295, (45931, 45961)),
("2026-01-05", "2025-01_2025-12", 45658, 46022, (45658, 45688)),
("2026-03-31", "2025-03_2026-02", 45717, 46081, (45717, 45747)),
] {
let f = build_grid_network_qlik_date_filter(d(jour), 12);
assert_eq!((f.label.as_str(), f.q_start, f.q_end, f.daily_serials.len()), (label, q_start, q_end, 365), "{jour}");
let mois = get_month_ranges(&f);
assert_eq!(mois.len(), 12, "{jour}");
assert_eq!((mois[0].q_start, mois[0].q_end), premier_mois, "{jour}");
assert_eq!(mois[11].q_end, q_end, "{jour}");
assert_eq!(mois.iter().map(|m| m.daily_serials.len()).sum::<usize>(), 365, "{jour}");
}
}
// [1] + [2] Repères Qlik et aller-retour.
#[test]
fn reperes_qlik() {
for (iso, serial) in QLIK_DATE_ANCHORS {
assert_eq!(iso_date_to_qlik_serial(iso).unwrap(), serial, "{iso}");
assert_eq!(qlik_serial_to_iso_date(serial), iso);
}
}
// [3] Fenêtre grille (now=2026-06-21) → 2025-06-01 .. 2026-05-31
#[test]
fn fenetre_juin_2026() {
let f = build_grid_network_qlik_date_filter(d("2026-06-21"), QLIK_MONTHS_BACK_DEFAULT);
assert_eq!(f.date_debut, "2025-06-01");
assert_eq!(f.date_fin, "2026-05-31");
assert_eq!(f.q_start, 45809);
assert_eq!(f.q_end, 46173);
assert_eq!(f.label, "2025-06_2026-05");
assert_eq!(f.set_analysis, r#"Date={">=45809<=46173"}"#);
assert_eq!(f.daily_serials.len(), 365);
assert_eq!(f.daily_serials[0], 45809);
assert_eq!(f.daily_serials[364], 46173);
}
// [4] et [5]
#[test]
fn fenetres_mars_et_bissextile() {
let f2 = build_grid_network_qlik_date_filter(d("2026-03-15"), 12);
assert_eq!((f2.date_debut.as_str(), f2.date_fin.as_str()), ("2025-03-01", "2026-02-28"));
assert_eq!((f2.q_start, f2.q_end), (45717, 46081));
let f3 = build_grid_network_qlik_date_filter(d("2025-01-05"), 12);
assert_eq!((f3.date_debut.as_str(), f3.date_fin.as_str()), ("2024-01-01", "2024-12-31"));
assert_eq!(f3.daily_serials.len(), 366);
}
// [6] chunkSerials
#[test]
fn decoupe_serials() {
let f = build_grid_network_qlik_date_filter(d("2026-06-21"), 12);
let chunks = chunk_serials(&f.daily_serials, 100);
assert_eq!(chunks.len(), 4);
assert_eq!(chunks[3].len(), 65);
assert_eq!(chunks[0][0], 45809);
assert_eq!(chunks[3][64], 46173);
}
// [7] Robustesse
#[test]
fn formats_invalides() {
for bad in ["2025/01/01", ""] {
let e = iso_date_to_qlik_serial(bad).unwrap_err().to_string();
assert!(e.contains("YYYY-MM-DD"), "{e}");
}
let f = build_grid_network_qlik_date_filter(d("2026-06-21"), 12);
assert!(!f.label.is_empty());
assert!(f.set_analysis.starts_with("Date="));
}
// [8] getMonthRanges — fenêtre 12 mois
#[test]
fn mois_de_la_fenetre() {
let f = build_grid_network_qlik_date_filter(d("2026-06-21"), 12);
let months = get_month_ranges(&f);
assert_eq!(months.len(), 12);
assert_eq!(months[0].label, "2025-06");
assert_eq!(months[0].date_debut, "2025-06-01");
assert_eq!(months[0].date_fin, "2025-06-30");
assert_eq!(months[0].daily_serials.len(), 30);
assert_eq!(months[0].q_start, 45809);
assert_eq!(months[0].q_end, 45838);
assert_eq!(months[11].label, "2026-05");
assert_eq!(months[11].date_debut, "2026-05-01");
assert_eq!(months[11].date_fin, "2026-05-31");
assert_eq!(months[11].daily_serials.len(), 31);
let feb25 = get_month_ranges(&build_grid_network_qlik_date_filter(d("2025-06-15"), 12));
assert!(feb25.iter().any(|m| m.label == "2025-02" && m.daily_serials.len() == 28));
let biss = get_month_ranges(&build_grid_network_qlik_date_filter(d("2025-01-05"), 12));
assert!(biss.iter().any(|m| m.label == "2024-02" && m.daily_serials.len() == 29));
assert_eq!(months.iter().map(|m| m.daily_serials.len()).sum::<usize>(), 365);
}
// [9] fenêtre courte partielle
#[test]
fn fenetre_partielle() {
let mut f = build_grid_network_qlik_date_filter(d("2026-06-21"), 12);
f.date_debut = "2025-06-01".into();
f.date_fin = "2025-08-15".into();
f.q_start = 45809;
f.q_end = 45884;
f.daily_serials.clear();
f.set_analysis.clear();
let m = get_month_ranges(&f);
assert_eq!(m.len(), 3);
assert_eq!(m[2].daily_serials.len(), 15);
assert_eq!(m[2].date_fin, "2025-08-15");
}
// [10] monthsBack=6
#[test]
fn six_mois() {
let f6 = build_grid_network_qlik_date_filter(d("2026-06-21"), 6);
assert_eq!(f6.date_debut, "2025-12-01");
assert_eq!(f6.date_fin, "2026-05-31");
assert_eq!((f6.q_start, f6.q_end), (45992, 46173));
assert_eq!(f6.label, "2025-12_2026-05");
assert_eq!(f6.daily_serials.len(), 182);
let m6 = get_month_ranges(&f6);
assert_eq!(m6.len(), 6);
assert_eq!(m6[0].label, "2025-12");
assert_eq!(m6[5].label, "2026-05");
assert_eq!(m6.iter().map(|m| m.daily_serials.len()).sum::<usize>(), 182);
}
// [11] bornes et clamp
#[test]
fn bornes_months_back() {
let now = d("2026-06-21");
let x = build_grid_network_qlik_date_filter(now, 1);
assert_eq!((x.date_debut.as_str(), x.date_fin.as_str()), ("2026-05-01", "2026-05-31"));
assert_eq!(build_grid_network_qlik_date_filter(now, 0).date_debut, "2026-05-01");
let x = build_grid_network_qlik_date_filter(now, 24);
assert_eq!((x.date_debut.as_str(), x.date_fin.as_str()), ("2025-06-01", "2026-05-31"));
assert_eq!(build_grid_network_qlik_date_filter(now, -5).date_debut, "2026-05-01");
assert_eq!((QLIK_MONTHS_BACK_MIN, QLIK_MONTHS_BACK_MAX, QLIK_MONTHS_BACK_DEFAULT), (1, 12, 12));
}
// [12] envMonthsBack — lecture env (cœur pur, sans toucher l'environnement du processus)
#[test]
fn lecture_env_months_back() {
let def = QLIK_MONTHS_BACK_DEFAULT;
assert_eq!(months_back_from(None, def), def);
assert_eq!(months_back_from(Some(""), def), def);
assert_eq!(months_back_from(Some("6"), def), 6);
assert_eq!(months_back_from(Some("abc"), def), def);
assert_eq!(months_back_from(Some("24"), def), QLIK_MONTHS_BACK_MAX);
assert_eq!(months_back_from(Some("-3"), def), QLIK_MONTHS_BACK_MIN);
assert_eq!(months_back_from(Some("0"), def), QLIK_MONTHS_BACK_MIN);
assert_eq!(months_back_from(Some("3.7"), def), 3);
assert_eq!(months_back_from(Some(" 8 "), def), 8);
assert_eq!(months_back_from(Some("NaN"), def), def);
assert_eq!(months_back_from(Some("Infinity"), def), def);
// Sur le vrai environnement : variable absente → défaut.
assert_eq!(env_months_back("QLIK_TEST_MONTHS_BACK_ABSENTE_XYZ", def), def);
}
#[test]
fn nombres_facon_js() {
assert_eq!(js_number(""), 0.0);
assert_eq!(js_number(" 12 "), 12.0);
assert_eq!(js_number("0x10"), 16.0);
assert_eq!(js_number("1e2"), 100.0);
assert!(js_number("inf").is_nan());
assert!(js_number("12abc").is_nan());
}
#[test]
fn serial_vers_aaaammjj() {
assert_eq!(qlik_serial_to_ymd(46192), 20260619);
}
}