Compare commits
2 Commits
1561c07432
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf89569702 | ||
|
|
6d4e682c60 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1007,6 +1007,7 @@ dependencies = [
|
|||||||
"fs_extra",
|
"fs_extra",
|
||||||
"geoutils",
|
"geoutils",
|
||||||
"gpx",
|
"gpx",
|
||||||
|
"ignore",
|
||||||
"notify",
|
"notify",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"regex",
|
"regex",
|
||||||
|
|||||||
@@ -22,3 +22,4 @@ fs_extra = "1"
|
|||||||
notify = "7"
|
notify = "7"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
tower-http = { version = "0.6", features = ["fs"] }
|
tower-http = { version = "0.6", features = ["fs"] }
|
||||||
|
ignore = "0.4"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -36,6 +38,13 @@ pub enum Commands {
|
|||||||
Dev { site: Site },
|
Dev { site: Site },
|
||||||
/// Déployer un site (build + rsync)
|
/// Déployer un site (build + rsync)
|
||||||
Deploy { site: Site },
|
Deploy { site: Site },
|
||||||
|
/// Analyser un export CSV d'index EDF et comparer les tarifs
|
||||||
|
Edf { csv: PathBuf },
|
||||||
|
/// Compter les lignes de code
|
||||||
|
Cloc {
|
||||||
|
/// Répertoire à analyser
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
|
|||||||
197
src/commands/cloc.rs
Normal file
197
src/commands/cloc.rs
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use ignore::WalkBuilder;
|
||||||
|
|
||||||
|
const CYAN: &str = "\x1b[36m";
|
||||||
|
const GREEN: &str = "\x1b[32m";
|
||||||
|
const YELLOW: &str = "\x1b[33m";
|
||||||
|
const RESET: &str = "\x1b[0m";
|
||||||
|
const SEPARATOR: &str = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━";
|
||||||
|
|
||||||
|
pub fn run(path: Option<PathBuf>) -> Result<()> {
|
||||||
|
let path = path.unwrap_or_else(|| env::current_dir().unwrap());
|
||||||
|
let mut languages: HashMap<&str, (usize, usize)> = HashMap::new();
|
||||||
|
let mut livewire_php_lines: usize = 0;
|
||||||
|
let mut livewire_blade_lines: usize = 0;
|
||||||
|
let mut livewire_files: usize = 0;
|
||||||
|
let mut test_lines: usize = 0;
|
||||||
|
let mut test_files: usize = 0;
|
||||||
|
|
||||||
|
for entry in WalkBuilder::new(&path).build() {
|
||||||
|
let entry = entry?;
|
||||||
|
if !entry.file_type().map_or(false, |ft| ft.is_file()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_path = entry.path();
|
||||||
|
let filename = file_path.file_name().unwrap_or_default().to_string_lossy();
|
||||||
|
|
||||||
|
if is_lock_file(&filename) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Livewire SFC: ⚡*.blade.php (excluding test files)
|
||||||
|
if filename.starts_with('⚡')
|
||||||
|
&& filename.ends_with(".blade.php")
|
||||||
|
&& !filename.ends_with(".test.php")
|
||||||
|
{
|
||||||
|
let Ok(content) = fs::read_to_string(file_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
livewire_files += 1;
|
||||||
|
let mut in_php = true;
|
||||||
|
for line in content.lines() {
|
||||||
|
if in_php && line.trim() == "?>" {
|
||||||
|
in_php = false;
|
||||||
|
} else if in_php {
|
||||||
|
livewire_php_lines += 1;
|
||||||
|
} else {
|
||||||
|
livewire_blade_lines += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP tests: *.test.php or .php in tests/ directory
|
||||||
|
let in_tests_dir = file_path.components().any(|c| c.as_os_str() == "tests");
|
||||||
|
if filename.ends_with(".test.php") || (in_tests_dir && filename.ends_with(".php")) {
|
||||||
|
let Ok(content) = fs::read_to_string(file_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
test_lines += content.lines().count();
|
||||||
|
test_files += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blade templates: *.blade.php (not Livewire SFC)
|
||||||
|
if filename.ends_with(".blade.php") {
|
||||||
|
let Ok(content) = fs::read_to_string(file_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let entry = languages.entry("Blade").or_insert((0, 0));
|
||||||
|
entry.0 += content.lines().count();
|
||||||
|
entry.1 += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(lang) = detect_language(file_path) {
|
||||||
|
let Ok(content) = fs::read_to_string(file_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let entry = languages.entry(lang).or_insert((0, 0));
|
||||||
|
entry.0 += content.lines().count();
|
||||||
|
entry.1 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sorted: Vec<_> = languages.into_iter().collect();
|
||||||
|
sorted.sort_by(|a, b| b.1 .0.cmp(&a.1 .0));
|
||||||
|
|
||||||
|
println!("{CYAN}📊 Statistiques du code{RESET}");
|
||||||
|
println!("{SEPARATOR}");
|
||||||
|
|
||||||
|
let mut total_lines: usize = 0;
|
||||||
|
let mut total_files: usize = 0;
|
||||||
|
|
||||||
|
if livewire_files > 0 {
|
||||||
|
print_line(
|
||||||
|
GREEN,
|
||||||
|
"Livewire SFC (PHP)",
|
||||||
|
livewire_php_lines,
|
||||||
|
Some(livewire_files),
|
||||||
|
);
|
||||||
|
print_line(GREEN, "Livewire SFC (Blade)", livewire_blade_lines, None);
|
||||||
|
total_lines += livewire_php_lines + livewire_blade_lines;
|
||||||
|
total_files += livewire_files;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (lang, (lines, files)) in &sorted {
|
||||||
|
print_line(GREEN, lang, *lines, Some(*files));
|
||||||
|
total_lines += lines;
|
||||||
|
total_files += files;
|
||||||
|
}
|
||||||
|
|
||||||
|
if test_files > 0 {
|
||||||
|
print_line(YELLOW, "Tests", test_lines, Some(test_files));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{SEPARATOR}");
|
||||||
|
print_line(CYAN, "Total (hors tests)", total_lines, Some(total_files));
|
||||||
|
print_line(
|
||||||
|
CYAN,
|
||||||
|
"Total",
|
||||||
|
total_lines + test_lines,
|
||||||
|
Some(total_files + test_files),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_line(color: &str, name: &str, lines: usize, files: Option<usize>) {
|
||||||
|
match files {
|
||||||
|
Some(f) => println!(
|
||||||
|
"{color}{:<25}{RESET} {:>6} lignes ({f} fichiers)",
|
||||||
|
name, lines
|
||||||
|
),
|
||||||
|
None => println!("{color}{:<25}{RESET} {:>6} lignes", name, lines),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_lock_file(filename: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
filename,
|
||||||
|
"composer.lock"
|
||||||
|
| "package-lock.json"
|
||||||
|
| "yarn.lock"
|
||||||
|
| "pnpm-lock.yaml"
|
||||||
|
| "Cargo.lock"
|
||||||
|
| "Gemfile.lock"
|
||||||
|
| "poetry.lock"
|
||||||
|
| "flake.lock"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_language(path: &Path) -> Option<&'static str> {
|
||||||
|
let filename = path.file_name()?.to_str()?;
|
||||||
|
match filename {
|
||||||
|
"Dockerfile" | "Containerfile" => return Some("Docker"),
|
||||||
|
"Makefile" | "GNUmakefile" => return Some("Makefile"),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ext = path.extension()?.to_str()?;
|
||||||
|
match ext {
|
||||||
|
"php" => Some("PHP"),
|
||||||
|
"js" | "mjs" | "cjs" => Some("JavaScript"),
|
||||||
|
"ts" | "mts" | "cts" => Some("TypeScript"),
|
||||||
|
"jsx" => Some("JSX"),
|
||||||
|
"tsx" => Some("TSX"),
|
||||||
|
"rs" => Some("Rust"),
|
||||||
|
"py" | "pyw" => Some("Python"),
|
||||||
|
"rb" => Some("Ruby"),
|
||||||
|
"go" => Some("Go"),
|
||||||
|
"java" => Some("Java"),
|
||||||
|
"kt" | "kts" => Some("Kotlin"),
|
||||||
|
"swift" => Some("Swift"),
|
||||||
|
"c" | "h" => Some("C"),
|
||||||
|
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some("C++"),
|
||||||
|
"cs" => Some("C#"),
|
||||||
|
"hs" | "lhs" => Some("Haskell"),
|
||||||
|
"ex" | "exs" => Some("Elixir"),
|
||||||
|
"erl" | "hrl" => Some("Erlang"),
|
||||||
|
"dart" => Some("Dart"),
|
||||||
|
"lua" => Some("Lua"),
|
||||||
|
"r" | "R" => Some("R"),
|
||||||
|
"scala" | "sc" => Some("Scala"),
|
||||||
|
"zig" => Some("Zig"),
|
||||||
|
"sh" | "bash" | "zsh" | "fish" => Some("Shell"),
|
||||||
|
"sql" => Some("SQL"),
|
||||||
|
"vue" => Some("Vue"),
|
||||||
|
"svelte" => Some("Svelte"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
348
src/commands/edf.rs
Normal file
348
src/commands/edf.rs
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{Result, bail};
|
||||||
|
|
||||||
|
struct Reading {
|
||||||
|
date: String,
|
||||||
|
hc_bleu: u64,
|
||||||
|
hp_bleu: u64,
|
||||||
|
hc_blanc: u64,
|
||||||
|
hp_blanc: u64,
|
||||||
|
hc_rouge: u64,
|
||||||
|
hp_rouge: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Consumption {
|
||||||
|
hc_bleu: u64,
|
||||||
|
hp_bleu: u64,
|
||||||
|
hc_blanc: u64,
|
||||||
|
hp_blanc: u64,
|
||||||
|
hc_rouge: u64,
|
||||||
|
hp_rouge: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Consumption {
|
||||||
|
fn total(&self) -> u64 {
|
||||||
|
self.hc_bleu + self.hp_bleu + self.hc_blanc + self.hp_blanc + self.hc_rouge + self.hp_rouge
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_hc(&self) -> u64 {
|
||||||
|
self.hc_bleu + self.hc_blanc + self.hc_rouge
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_hp(&self) -> u64 {
|
||||||
|
self.hp_bleu + self.hp_blanc + self.hp_rouge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tarifs EDF 6 kVA (€/kWh et €/mois)
|
||||||
|
const BASE_ABO: f64 = 15.47;
|
||||||
|
const BASE_KWH: f64 = 0.1952;
|
||||||
|
|
||||||
|
const HPHC_ABO: f64 = 15.74;
|
||||||
|
const HPHC_HP: f64 = 0.2081;
|
||||||
|
const HPHC_HC: f64 = 0.1635;
|
||||||
|
|
||||||
|
const TEMPO_ABO: f64 = 15.50;
|
||||||
|
const TEMPO_BLEU_HC: f64 = 0.1232;
|
||||||
|
const TEMPO_BLEU_HP: f64 = 0.1494;
|
||||||
|
const TEMPO_BLANC_HC: f64 = 0.1391;
|
||||||
|
const TEMPO_BLANC_HP: f64 = 0.1730;
|
||||||
|
const TEMPO_ROUGE_HC: f64 = 0.1460;
|
||||||
|
const TEMPO_ROUGE_HP: f64 = 0.6468;
|
||||||
|
|
||||||
|
pub fn run(csv_path: &Path) -> Result<()> {
|
||||||
|
let raw = fs::read(csv_path)?;
|
||||||
|
let content = String::from_utf8(raw.clone())
|
||||||
|
.unwrap_or_else(|_| raw.iter().map(|&b| b as char).collect());
|
||||||
|
|
||||||
|
let readings = parse_readings(&content)?;
|
||||||
|
|
||||||
|
if readings.len() < 2 {
|
||||||
|
bail!("pas assez de relevés dans le fichier");
|
||||||
|
}
|
||||||
|
|
||||||
|
let last = readings.first().unwrap();
|
||||||
|
|
||||||
|
// Chercher le relevé le plus proche de last.date - 1 an
|
||||||
|
let target_date = one_year_before(&last.date);
|
||||||
|
let start = readings
|
||||||
|
.iter()
|
||||||
|
.min_by_key(|r| date_distance(&r.date, &target_date))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let conso = Consumption {
|
||||||
|
hc_bleu: last.hc_bleu - start.hc_bleu,
|
||||||
|
hp_bleu: last.hp_bleu - start.hp_bleu,
|
||||||
|
hc_blanc: last.hc_blanc - start.hc_blanc,
|
||||||
|
hp_blanc: last.hp_blanc - start.hp_blanc,
|
||||||
|
hc_rouge: last.hc_rouge - start.hc_rouge,
|
||||||
|
hp_rouge: last.hp_rouge - start.hp_rouge,
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("Période : {} → {} ({} relevés)", start.date, last.date, readings.len());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
print_consumption(&conso);
|
||||||
|
println!();
|
||||||
|
|
||||||
|
print_tariffs(&conso);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_readings(content: &str) -> Result<Vec<Reading>> {
|
||||||
|
let mut readings = Vec::new();
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let parts: Vec<&str> = line.split(';').collect();
|
||||||
|
if parts.len() != 8 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(hc_bleu) = parts[2].parse::<u64>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
readings.push(Reading {
|
||||||
|
date: parts[0].to_string(),
|
||||||
|
hc_bleu,
|
||||||
|
hp_bleu: parts[3].parse().unwrap_or(0),
|
||||||
|
hc_blanc: parts[4].parse().unwrap_or(0),
|
||||||
|
hp_blanc: parts[5].parse().unwrap_or(0),
|
||||||
|
hc_rouge: parts[6].parse().unwrap_or(0),
|
||||||
|
hp_rouge: parts[7].parse().unwrap_or(0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(readings)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_consumption(c: &Consumption) {
|
||||||
|
let total = c.total() as f64;
|
||||||
|
|
||||||
|
println!("Consommation sur la période :");
|
||||||
|
println!();
|
||||||
|
println!(" {:10} {:>8} {:>8}", "Type", "kWh", "%");
|
||||||
|
println!(" {:10} {:>8} {:>8}", "──────────", "────────", "────────");
|
||||||
|
print_conso_line("HC Bleu", c.hc_bleu, total);
|
||||||
|
print_conso_line("HP Bleu", c.hp_bleu, total);
|
||||||
|
print_conso_line("HC Blanc", c.hc_blanc, total);
|
||||||
|
print_conso_line("HP Blanc", c.hp_blanc, total);
|
||||||
|
print_conso_line("HC Rouge", c.hc_rouge, total);
|
||||||
|
print_conso_line("HP Rouge", c.hp_rouge, total);
|
||||||
|
println!(" {:10} {:>8} {:>8}", "──────────", "────────", "────────");
|
||||||
|
println!(" {:10} {:>8}", "TOTAL", format_kwh(c.total()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_conso_line(label: &str, kwh: u64, total: f64) {
|
||||||
|
let pct = format!("{:.1}", (kwh as f64 / total) * 100.0).replace('.', ",");
|
||||||
|
println!(" {:10} {:>8} {:>7}%", label, format_kwh(kwh), pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_tariffs(c: &Consumption) {
|
||||||
|
let total = c.total();
|
||||||
|
|
||||||
|
let base_abo = BASE_ABO * 12.0;
|
||||||
|
let base_conso = total as f64 * BASE_KWH;
|
||||||
|
let hphc_abo = HPHC_ABO * 12.0;
|
||||||
|
let tempo_abo = TEMPO_ABO * 12.0;
|
||||||
|
|
||||||
|
let (base_total, hphc_total, tempo_total) = compute_tariffs(c);
|
||||||
|
|
||||||
|
println!("Comparatif des tarifs (6 kVA, 12 mois) :");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!(" Base :");
|
||||||
|
print_tariff_line("Abonnement", base_abo, 0, 0.0);
|
||||||
|
print_tariff_line("Consommation", base_conso, total, BASE_KWH);
|
||||||
|
println!(" {:20} {:>9} €", "Total", format_eur(base_total));
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!(" HP/HC :");
|
||||||
|
print_tariff_line("Abonnement", hphc_abo, 0, 0.0);
|
||||||
|
print_tariff_line("Heures Pleines", c.total_hp() as f64 * HPHC_HP, c.total_hp(), HPHC_HP);
|
||||||
|
print_tariff_line("Heures Creuses", c.total_hc() as f64 * HPHC_HC, c.total_hc(), HPHC_HC);
|
||||||
|
println!(" {:20} {:>9} €", "Total", format_eur(hphc_total));
|
||||||
|
println!();
|
||||||
|
|
||||||
|
println!(" Tempo :");
|
||||||
|
print_tariff_line("Abonnement", tempo_abo, 0, 0.0);
|
||||||
|
print_tariff_line("Bleu HC", c.hc_bleu as f64 * TEMPO_BLEU_HC, c.hc_bleu, TEMPO_BLEU_HC);
|
||||||
|
print_tariff_line("Bleu HP", c.hp_bleu as f64 * TEMPO_BLEU_HP, c.hp_bleu, TEMPO_BLEU_HP);
|
||||||
|
print_tariff_line("Blanc HC", c.hc_blanc as f64 * TEMPO_BLANC_HC, c.hc_blanc, TEMPO_BLANC_HC);
|
||||||
|
print_tariff_line("Blanc HP", c.hp_blanc as f64 * TEMPO_BLANC_HP, c.hp_blanc, TEMPO_BLANC_HP);
|
||||||
|
print_tariff_line("Rouge HC", c.hc_rouge as f64 * TEMPO_ROUGE_HC, c.hc_rouge, TEMPO_ROUGE_HC);
|
||||||
|
print_tariff_line("Rouge HP", c.hp_rouge as f64 * TEMPO_ROUGE_HP, c.hp_rouge, TEMPO_ROUGE_HP);
|
||||||
|
println!(" {:20} {:>9} €", "Total", format_eur(tempo_total));
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Résumé
|
||||||
|
println!("Résumé :");
|
||||||
|
println!();
|
||||||
|
println!(" {:8} {:>12} {:>16}", "Option", "Coût annuel", "vs Base");
|
||||||
|
println!(" {:8} {:>12} {:>16}", "────────", "────────────", "────────────────");
|
||||||
|
|
||||||
|
let mut options = vec![
|
||||||
|
("Tempo", tempo_total),
|
||||||
|
("HP/HC", hphc_total),
|
||||||
|
("Base", base_total),
|
||||||
|
];
|
||||||
|
options.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||||
|
|
||||||
|
for (name, cost) in &options {
|
||||||
|
let diff = cost - base_total;
|
||||||
|
let pct = (diff / base_total) * 100.0;
|
||||||
|
if *name == "Base" {
|
||||||
|
println!(" {:8} {:>10} € référence", name, format_eur(*cost));
|
||||||
|
} else {
|
||||||
|
println!(" {:8} {:>10} € {} € ({:+.0}%)", name, format_eur(*cost), format_eur(diff), pct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("Coût moyen au kWh ({} kWh) :", format_kwh(total));
|
||||||
|
println!();
|
||||||
|
for (name, cost) in &options {
|
||||||
|
println!(" {:8} {} cts/kWh", name, format_eur((cost / total as f64) * 100.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_tariff_line(label: &str, cost: f64, kwh: u64, rate: f64) {
|
||||||
|
if kwh == 0 {
|
||||||
|
println!(" {:20} {:>9} €", label, format_eur(cost));
|
||||||
|
} else {
|
||||||
|
println!(" {:20} {:>9} € ({:>5} kWh × {} €)", label, format_eur(cost), format_kwh(kwh), format_rate(rate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_eur(value: f64) -> String {
|
||||||
|
let abs = value.abs();
|
||||||
|
let integer = abs as u64;
|
||||||
|
let decimals = format!("{:.2}", abs.fract()).trim_start_matches('0').replace('.', ",");
|
||||||
|
let sign = if value < 0.0 { "-" } else { "" };
|
||||||
|
|
||||||
|
if integer >= 1000 {
|
||||||
|
format!("{}{} {:03}{}", sign, integer / 1000, integer % 1000, decimals)
|
||||||
|
} else {
|
||||||
|
format!("{}{}{}", sign, integer, decimals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_rate(value: f64) -> String {
|
||||||
|
format!("{:.4}", value).replace('.', ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_kwh(kwh: u64) -> String {
|
||||||
|
if kwh >= 1000 {
|
||||||
|
let thousands = kwh / 1000;
|
||||||
|
let rest = kwh % 1000;
|
||||||
|
format!("{} {:03}", thousands, rest)
|
||||||
|
} else {
|
||||||
|
kwh.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DD/MM/YYYY → (YYYY, MM, DD)
|
||||||
|
fn parse_date(date: &str) -> (i32, u32, u32) {
|
||||||
|
let parts: Vec<&str> = date.split('/').collect();
|
||||||
|
let day: u32 = parts[0].parse().unwrap();
|
||||||
|
let month: u32 = parts[1].parse().unwrap();
|
||||||
|
let year: i32 = parts[2].parse().unwrap();
|
||||||
|
(year, month, day)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn one_year_before(date: &str) -> String {
|
||||||
|
let (year, month, day) = parse_date(date);
|
||||||
|
format!("{:02}/{:02}/{}", day, month, year - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn date_distance(a: &str, b: &str) -> i32 {
|
||||||
|
let (ya, ma, da) = parse_date(a);
|
||||||
|
let (yb, mb, db) = parse_date(b);
|
||||||
|
let days_a = ya * 365 + ma as i32 * 30 + da as i32;
|
||||||
|
let days_b = yb * 365 + mb as i32 * 30 + db as i32;
|
||||||
|
(days_a - days_b).abs()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_tariffs(c: &Consumption) -> (f64, f64, f64) {
|
||||||
|
let total = c.total();
|
||||||
|
|
||||||
|
let base_total = BASE_ABO * 12.0 + total as f64 * BASE_KWH;
|
||||||
|
|
||||||
|
let hphc_total =
|
||||||
|
HPHC_ABO * 12.0 + c.total_hp() as f64 * HPHC_HP + c.total_hc() as f64 * HPHC_HC;
|
||||||
|
|
||||||
|
let tempo_total = TEMPO_ABO * 12.0
|
||||||
|
+ c.hc_bleu as f64 * TEMPO_BLEU_HC
|
||||||
|
+ c.hp_bleu as f64 * TEMPO_BLEU_HP
|
||||||
|
+ c.hc_blanc as f64 * TEMPO_BLANC_HC
|
||||||
|
+ c.hp_blanc as f64 * TEMPO_BLANC_HP
|
||||||
|
+ c.hc_rouge as f64 * TEMPO_ROUGE_HC
|
||||||
|
+ c.hp_rouge as f64 * TEMPO_ROUGE_HP;
|
||||||
|
|
||||||
|
(base_total, hphc_total, tempo_total)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_and_tariffs() {
|
||||||
|
let csv = "\
|
||||||
|
Récapitulatif\n\
|
||||||
|
\n\
|
||||||
|
Date;Type;HC Bleu;HP Bleu;HC Blanc;HP Blanc;HC Rouge;HP Rouge\n\
|
||||||
|
01/01/2025;Index;13000;6500;2800;1400;800;500\n\
|
||||||
|
15/06/2024;Index;11500;5700;2400;1200;650;400\n\
|
||||||
|
01/01/2024;Index;10000;5000;2000;1000;500;300\n";
|
||||||
|
|
||||||
|
let readings = parse_readings(csv).unwrap();
|
||||||
|
assert_eq!(readings.len(), 3);
|
||||||
|
|
||||||
|
let last = readings.first().unwrap();
|
||||||
|
let target_date = one_year_before(&last.date);
|
||||||
|
let start = readings
|
||||||
|
.iter()
|
||||||
|
.min_by_key(|r| date_distance(&r.date, &target_date))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(last.date, "01/01/2025");
|
||||||
|
assert_eq!(start.date, "01/01/2024");
|
||||||
|
|
||||||
|
let conso = Consumption {
|
||||||
|
hc_bleu: last.hc_bleu - start.hc_bleu,
|
||||||
|
hp_bleu: last.hp_bleu - start.hp_bleu,
|
||||||
|
hc_blanc: last.hc_blanc - start.hc_blanc,
|
||||||
|
hp_blanc: last.hp_blanc - start.hp_blanc,
|
||||||
|
hc_rouge: last.hc_rouge - start.hc_rouge,
|
||||||
|
hp_rouge: last.hp_rouge - start.hp_rouge,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(conso.hc_bleu, 3000);
|
||||||
|
assert_eq!(conso.hp_bleu, 1500);
|
||||||
|
assert_eq!(conso.hc_blanc, 800);
|
||||||
|
assert_eq!(conso.hp_blanc, 400);
|
||||||
|
assert_eq!(conso.hc_rouge, 300);
|
||||||
|
assert_eq!(conso.hp_rouge, 200);
|
||||||
|
assert_eq!(conso.total(), 6200);
|
||||||
|
|
||||||
|
let (base, hphc, tempo) = compute_tariffs(&conso);
|
||||||
|
|
||||||
|
assert!((base - 1395.88).abs() < 0.01);
|
||||||
|
assert!((hphc - 1296.24).abs() < 0.01);
|
||||||
|
assert!((tempo - 1133.34).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_utf8_parsing() {
|
||||||
|
let csv = "Récapitulatif\n\nDate;Type;HC Bleu;HP Bleu;HC Blanc;HP Blanc;HC Rouge;HP Rouge\n01/01/2024;Index;1000;500;100;50;10;5\n01/01/2025;Index;2000;800;200;100;30;15\n";
|
||||||
|
let readings = parse_readings(csv).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(readings.len(), 2);
|
||||||
|
assert_eq!(readings[0].hc_bleu, 1000);
|
||||||
|
assert_eq!(readings[1].hc_bleu, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
pub mod desktop;
|
pub mod desktop;
|
||||||
pub mod dev;
|
pub mod dev;
|
||||||
pub mod deploy;
|
pub mod deploy;
|
||||||
|
pub mod edf;
|
||||||
pub mod site;
|
pub mod site;
|
||||||
|
pub mod cloc;
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ async fn main() -> Result<()> {
|
|||||||
Commands::Desktop(cmd) => commands::desktop::run(&cmd),
|
Commands::Desktop(cmd) => commands::desktop::run(&cmd),
|
||||||
Commands::Dev { site } => commands::dev::run(site).await,
|
Commands::Dev { site } => commands::dev::run(site).await,
|
||||||
Commands::Deploy { site } => commands::deploy::run(site).await,
|
Commands::Deploy { site } => commands::deploy::run(site).await,
|
||||||
|
Commands::Edf { csv } => commands::edf::run(&csv),
|
||||||
|
Commands::Cloc { path } => commands::cloc::run(path),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user