refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
Re-sincroniza las fuentes desde el monorepo (estaba en vello 0.5/wgpu 24 y con la estructura vieja de eventloop) y suma el 3D: - bump del workspace a vello 0.7 / wgpu 27 / parley 0.6, + accesskit 0.24 / accesskit_winit 0.33 / vello_hybrid 0.0.9. - nuevos crates: llimphi-3d (voxels ray-march + mallas en un depth compartido, montable dentro de un View 2D vía set_viewport+scissor) y llimphi-voxel (world-gen, personajes, director de escenas) + shared/foreign-vox (puente .vox). - README: sección "Not just 2D — a 3D voxel engine" + GIF (docs/llimphi_voxel.gif). - excluido modules/allichay (arrastra deps fuera del alcance del front-door). - cargo check --workspace: verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "llimphi-widget-calendar"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "llimphi-widget-calendar — vista mensual del calendario (grilla 7×6) con navegación entre meses, día seleccionado y día actual resaltados. Base del date-picker; útil también solo para agendas, planning y ERP."
|
||||
|
||||
[dependencies]
|
||||
llimphi-ui = { workspace = true }
|
||||
llimphi-theme = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
@@ -0,0 +1,483 @@
|
||||
//! `llimphi-widget-calendar` — vista mensual del calendario.
|
||||
//!
|
||||
//! Una grilla 7 (columnas = días de la semana) × 6 (filas = semanas)
|
||||
//! mostrando los días del mes en foco, con header de mes/año + flechas de
|
||||
//! navegación, fila de iniciales de día, día seleccionado y día actual
|
||||
//! resaltados. El widget **no tiene estado propio**: el mes en foco, la
|
||||
//! selección y la fecha "hoy" viven en el `Model` del caller; el widget
|
||||
//! emite `Msg`s para navegar y para seleccionar.
|
||||
//!
|
||||
//! Base del **date-picker** (un `field` + un overlay que muestra este
|
||||
//! calendar) y útil por sí solo para agendas, planning, ERP. La lógica
|
||||
//! del calendario (días del mes, primer día de la semana, grilla 6×7) es
|
||||
//! pública y testeable: ver [`days_in_month`], [`first_weekday`],
|
||||
//! [`month_grid`].
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use chrono::{Datelike, NaiveDate, Weekday};
|
||||
use llimphi_ui::llimphi_layout::taffy::{
|
||||
prelude::{length, percent, FlexDirection, Size, Style},
|
||||
AlignItems, JustifyContent,
|
||||
};
|
||||
use llimphi_ui::llimphi_raster::peniko::Color;
|
||||
use llimphi_ui::View;
|
||||
use llimphi_theme::Theme;
|
||||
|
||||
/// Día de la semana con el que arranca la grilla. La mayor parte del mundo
|
||||
/// usa `Monday`; EE. UU. usa `Sunday`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WeekStart {
|
||||
Monday,
|
||||
Sunday,
|
||||
}
|
||||
|
||||
impl Default for WeekStart {
|
||||
fn default() -> Self {
|
||||
Self::Monday
|
||||
}
|
||||
}
|
||||
|
||||
impl WeekStart {
|
||||
/// Posición (0..7) del `weekday` empezando por este `WeekStart`. Con
|
||||
/// `Monday`: `Mon=0, Tue=1, …, Sun=6`. Con `Sunday`: `Sun=0, Mon=1,
|
||||
/// …, Sat=6`.
|
||||
pub fn index(self, w: Weekday) -> u32 {
|
||||
let mon0 = w.num_days_from_monday();
|
||||
match self {
|
||||
Self::Monday => mon0,
|
||||
// Domingo (mon0=6) pasa a 0, lunes (0) a 1, …
|
||||
Self::Sunday => (mon0 + 1) % 7,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iniciales de los siete días en el orden que dicta este `WeekStart`,
|
||||
/// con `locale = "es"` (L/M/M/J/V/S/D para Monday-start). Suficiente
|
||||
/// para v1; un mapa por `Theme.locale` se agrega cuando alguna app lo
|
||||
/// pida.
|
||||
pub fn weekday_initials_es(self) -> [&'static str; 7] {
|
||||
match self {
|
||||
Self::Monday => ["L", "M", "M", "J", "V", "S", "D"],
|
||||
Self::Sunday => ["D", "L", "M", "M", "J", "V", "S"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cantidad de días del mes `month` (1..=12) del año `year`. Devuelve `0`
|
||||
/// si `month` está fuera de rango (defensa contra `u32` arbitrario; un
|
||||
/// caller correcto pasa 1..=12).
|
||||
pub fn days_in_month(year: i32, month: u32) -> u32 {
|
||||
if !(1..=12).contains(&month) {
|
||||
return 0;
|
||||
}
|
||||
// Trick estándar: primer día del mes siguiente menos un día. Año
|
||||
// siguiente si month == 12. `from_ymd_opt` retorna `Some` siempre que
|
||||
// (year, month, day) sean válidos — para day=1 con month 1..=12 lo es.
|
||||
let (y2, m2) = if month == 12 {
|
||||
(year + 1, 1)
|
||||
} else {
|
||||
(year, month + 1)
|
||||
};
|
||||
let first_next = NaiveDate::from_ymd_opt(y2, m2, 1).expect("primer día válido");
|
||||
let first_this = NaiveDate::from_ymd_opt(year, month, 1).expect("primer día válido");
|
||||
first_next.signed_duration_since(first_this).num_days() as u32
|
||||
}
|
||||
|
||||
/// Día de la semana del **primer día** del mes (1 del mes). Sólo útil para
|
||||
/// quien quiera el `Weekday` crudo; la grilla usa [`first_weekday_index`].
|
||||
pub fn first_weekday(year: i32, month: u32) -> Option<Weekday> {
|
||||
NaiveDate::from_ymd_opt(year, month, 1).map(|d| d.weekday())
|
||||
}
|
||||
|
||||
/// Índice de columna (0..7) del primer día del mes según `WeekStart`. Es
|
||||
/// la cantidad de celdas vacías ANTES del 1 en la primera fila de la
|
||||
/// grilla. Devuelve `0` si el mes es inválido.
|
||||
pub fn first_weekday_index(year: i32, month: u32, start: WeekStart) -> u32 {
|
||||
first_weekday(year, month).map(|w| start.index(w)).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Grilla mensual de **6 filas × 7 columnas** con los días del mes. Las
|
||||
/// celdas que caen antes del primer día o después del último día del mes
|
||||
/// son `None`. Siempre seis filas: para meses cortos (28 días arrancando
|
||||
/// en domingo bajo Monday-start, p. ej. febrero 2021) la última fila queda
|
||||
/// vacía; mantener seis filas estables evita reflow al navegar entre
|
||||
/// meses (toda la grilla mide igual). Si `month` es inválido devuelve seis
|
||||
/// filas de siete `None`s.
|
||||
pub fn month_grid(year: i32, month: u32, start: WeekStart) -> [[Option<u32>; 7]; 6] {
|
||||
let mut grid: [[Option<u32>; 7]; 6] = [[None; 7]; 6];
|
||||
let dim = days_in_month(year, month);
|
||||
if dim == 0 {
|
||||
return grid;
|
||||
}
|
||||
let offset = first_weekday_index(year, month, start) as usize;
|
||||
for day in 1..=dim {
|
||||
let cell = offset + (day as usize) - 1;
|
||||
let r = cell / 7;
|
||||
let c = cell % 7;
|
||||
if r < 6 {
|
||||
grid[r][c] = Some(day);
|
||||
}
|
||||
// Si r == 6 (no debería pasar para meses estándar: 31+6=37<42),
|
||||
// se ignora; la grilla de 6×7 = 42 celdas alcanza para el peor caso
|
||||
// (31 días con offset = 6 → última celda 36, fila 5).
|
||||
}
|
||||
grid
|
||||
}
|
||||
|
||||
/// Avanza `(year, month)` un mes adelante (`delta = +1`) o atrás (`-1`),
|
||||
/// wrappeando años. Helper para los botones `<`/`>` del header.
|
||||
pub fn shift_month(year: i32, month: u32, delta: i32) -> (i32, u32) {
|
||||
let total = (year * 12 + month as i32 - 1) + delta;
|
||||
let y = total.div_euclid(12);
|
||||
let m = (total.rem_euclid(12) + 1) as u32;
|
||||
(y, m)
|
||||
}
|
||||
|
||||
/// Paleta del calendario.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CalendarPalette {
|
||||
/// Fondo del panel.
|
||||
pub bg: Color,
|
||||
/// Texto normal de los días del mes en foco.
|
||||
pub fg: Color,
|
||||
/// Texto de las iniciales de día / mes/año del header (atenuado).
|
||||
pub fg_muted: Color,
|
||||
/// Hover sobre una celda de día.
|
||||
pub hover_bg: Color,
|
||||
/// Fondo de la celda del día seleccionado.
|
||||
pub selected_bg: Color,
|
||||
/// Texto del día seleccionado (contraste contra `selected_bg`).
|
||||
pub selected_fg: Color,
|
||||
/// Color del borde de la celda "hoy" (sólo borde — el fondo sigue al
|
||||
/// hover/selected normal).
|
||||
pub today_border: Color,
|
||||
}
|
||||
|
||||
impl CalendarPalette {
|
||||
pub fn from_theme(t: &Theme) -> Self {
|
||||
Self {
|
||||
bg: t.bg_panel,
|
||||
fg: t.fg_text,
|
||||
fg_muted: t.fg_muted,
|
||||
hover_bg: t.bg_row_hover,
|
||||
selected_bg: t.accent,
|
||||
// El texto sobre `accent` se elige por contraste — el theme no
|
||||
// expone un `fg_on_accent` dedicado, así que tomamos `bg_app`
|
||||
// (oscuro en dark theme, claro en light theme) que es el
|
||||
// inverso natural del `fg_text`.
|
||||
selected_fg: t.bg_app,
|
||||
today_border: t.accent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CalendarPalette {
|
||||
fn default() -> Self {
|
||||
Self::from_theme(&Theme::dark())
|
||||
}
|
||||
}
|
||||
|
||||
/// Especificación del calendario para una vista mensual.
|
||||
pub struct CalendarSpec<Msg> {
|
||||
/// Año del mes en foco.
|
||||
pub view_year: i32,
|
||||
/// Mes en foco (1..=12).
|
||||
pub view_month: u32,
|
||||
/// Día seleccionado (opcional; si cae en otro mes no se resalta).
|
||||
pub selected: Option<NaiveDate>,
|
||||
/// Fecha "hoy" (opcional; si cae en otro mes no se resalta). El caller
|
||||
/// la inyecta — el widget no toca el reloj para mantenerse puro y
|
||||
/// testeable.
|
||||
pub today: Option<NaiveDate>,
|
||||
/// Primer día de la semana en la grilla.
|
||||
pub week_start: WeekStart,
|
||||
pub palette: CalendarPalette,
|
||||
/// `Msg` para "seleccioné el día X (NaiveDate del mes en foco)".
|
||||
pub on_select: std::sync::Arc<dyn Fn(NaiveDate) -> Msg + Send + Sync>,
|
||||
/// `Msg` para "muévete al mes (year, month)" — disparado por `<` / `>`.
|
||||
pub on_view_change: std::sync::Arc<dyn Fn(i32, u32) -> Msg + Send + Sync>,
|
||||
}
|
||||
|
||||
/// Nombre del mes en español (1..=12). Para v1; un map por `Theme.locale`
|
||||
/// se agrega cuando alguna app lo pida.
|
||||
fn month_name_es(m: u32) -> &'static str {
|
||||
match m {
|
||||
1 => "enero",
|
||||
2 => "febrero",
|
||||
3 => "marzo",
|
||||
4 => "abril",
|
||||
5 => "mayo",
|
||||
6 => "junio",
|
||||
7 => "julio",
|
||||
8 => "agosto",
|
||||
9 => "septiembre",
|
||||
10 => "octubre",
|
||||
11 => "noviembre",
|
||||
12 => "diciembre",
|
||||
_ => "—",
|
||||
}
|
||||
}
|
||||
|
||||
const HEADER_H: f32 = 32.0;
|
||||
const ROW_H: f32 = 32.0;
|
||||
const CELL_W: f32 = 32.0;
|
||||
const PAD: f32 = 8.0;
|
||||
|
||||
/// Vista del calendario. Devuelve un `View<Msg>` autocontenido (panel +
|
||||
/// header + week labels + grilla 6×7), de ancho fijo `7 * CELL_W + 2 * PAD`.
|
||||
pub fn calendar_view<Msg>(spec: CalendarSpec<Msg>) -> View<Msg>
|
||||
where
|
||||
Msg: Clone + 'static,
|
||||
{
|
||||
let CalendarSpec {
|
||||
view_year,
|
||||
view_month,
|
||||
selected,
|
||||
today,
|
||||
week_start,
|
||||
palette,
|
||||
on_select,
|
||||
on_view_change,
|
||||
} = spec;
|
||||
|
||||
// Header: < Mes Año >
|
||||
let prev = {
|
||||
let f = on_view_change.clone();
|
||||
let (py, pm) = shift_month(view_year, view_month, -1);
|
||||
View::<Msg>::new(Style {
|
||||
size: Size { width: length(CELL_W), height: length(HEADER_H) },
|
||||
align_items: Some(AlignItems::Center),
|
||||
justify_content: Some(JustifyContent::Center),
|
||||
..Default::default()
|
||||
})
|
||||
.text("‹", 18.0, palette.fg_muted)
|
||||
.hover_fill(palette.hover_bg)
|
||||
.radius(6.0)
|
||||
.cursor(llimphi_ui::Cursor::Pointer)
|
||||
.on_click_at({
|
||||
let f = f.clone();
|
||||
move |_, _, _, _| Some(f(py, pm))
|
||||
})
|
||||
};
|
||||
let next = {
|
||||
let f = on_view_change.clone();
|
||||
let (ny, nm) = shift_month(view_year, view_month, 1);
|
||||
View::<Msg>::new(Style {
|
||||
size: Size { width: length(CELL_W), height: length(HEADER_H) },
|
||||
align_items: Some(AlignItems::Center),
|
||||
justify_content: Some(JustifyContent::Center),
|
||||
..Default::default()
|
||||
})
|
||||
.text("›", 18.0, palette.fg_muted)
|
||||
.hover_fill(palette.hover_bg)
|
||||
.radius(6.0)
|
||||
.cursor(llimphi_ui::Cursor::Pointer)
|
||||
.on_click_at(move |_, _, _, _| Some(f(ny, nm)))
|
||||
};
|
||||
let title = format!("{} {}", month_name_es(view_month), view_year);
|
||||
let title_box = View::<Msg>::new(Style {
|
||||
size: Size { width: percent(1.0), height: length(HEADER_H) },
|
||||
flex_grow: 1.0,
|
||||
align_items: Some(AlignItems::Center),
|
||||
justify_content: Some(JustifyContent::Center),
|
||||
..Default::default()
|
||||
})
|
||||
.text(title, 14.0, palette.fg);
|
||||
let header = View::<Msg>::new(Style {
|
||||
flex_direction: FlexDirection::Row,
|
||||
align_items: Some(AlignItems::Center),
|
||||
size: Size { width: percent(1.0), height: length(HEADER_H) },
|
||||
..Default::default()
|
||||
})
|
||||
.children(vec![prev, title_box, next]);
|
||||
|
||||
// Week labels (L M M J V S D o D L M M J V S).
|
||||
let mut week_cols: Vec<View<Msg>> = Vec::with_capacity(7);
|
||||
for label in week_start.weekday_initials_es() {
|
||||
week_cols.push(
|
||||
View::<Msg>::new(Style {
|
||||
size: Size { width: length(CELL_W), height: length(ROW_H * 0.75) },
|
||||
align_items: Some(AlignItems::Center),
|
||||
justify_content: Some(JustifyContent::Center),
|
||||
..Default::default()
|
||||
})
|
||||
.text(label, 11.0, palette.fg_muted),
|
||||
);
|
||||
}
|
||||
let week_row = View::<Msg>::new(Style {
|
||||
flex_direction: FlexDirection::Row,
|
||||
size: Size {
|
||||
width: length(CELL_W * 7.0),
|
||||
height: length(ROW_H * 0.75),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.children(week_cols);
|
||||
|
||||
// Grilla 6×7.
|
||||
let grid = month_grid(view_year, view_month, week_start);
|
||||
let mut grid_rows: Vec<View<Msg>> = Vec::with_capacity(6);
|
||||
for row in grid.iter() {
|
||||
let mut cells: Vec<View<Msg>> = Vec::with_capacity(7);
|
||||
for cell in row.iter() {
|
||||
let view: View<Msg> = match *cell {
|
||||
None => View::<Msg>::new(Style {
|
||||
size: Size { width: length(CELL_W), height: length(ROW_H) },
|
||||
..Default::default()
|
||||
}),
|
||||
Some(day) => {
|
||||
let date = NaiveDate::from_ymd_opt(view_year, view_month, day)
|
||||
.expect("día válido del mes en foco");
|
||||
let is_sel = selected == Some(date);
|
||||
let is_today = today == Some(date);
|
||||
let fg = if is_sel { palette.selected_fg } else { palette.fg };
|
||||
let mut v = View::<Msg>::new(Style {
|
||||
size: Size { width: length(CELL_W), height: length(ROW_H) },
|
||||
align_items: Some(AlignItems::Center),
|
||||
justify_content: Some(JustifyContent::Center),
|
||||
..Default::default()
|
||||
})
|
||||
.text(day.to_string(), 13.0, fg)
|
||||
.radius(6.0)
|
||||
.cursor(llimphi_ui::Cursor::Pointer);
|
||||
if is_sel {
|
||||
v = v.fill(palette.selected_bg);
|
||||
} else {
|
||||
v = v.hover_fill(palette.hover_bg);
|
||||
}
|
||||
if is_today {
|
||||
v = v.border(1.0, palette.today_border);
|
||||
}
|
||||
let f = on_select.clone();
|
||||
v = v.on_click_at(move |_, _, _, _| Some(f(date)));
|
||||
v
|
||||
}
|
||||
};
|
||||
cells.push(view);
|
||||
}
|
||||
grid_rows.push(
|
||||
View::<Msg>::new(Style {
|
||||
flex_direction: FlexDirection::Row,
|
||||
size: Size { width: length(CELL_W * 7.0), height: length(ROW_H) },
|
||||
..Default::default()
|
||||
})
|
||||
.children(cells),
|
||||
);
|
||||
}
|
||||
let grid_box = View::<Msg>::new(Style {
|
||||
flex_direction: FlexDirection::Column,
|
||||
size: Size {
|
||||
width: length(CELL_W * 7.0),
|
||||
height: length(ROW_H * 6.0),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.children(grid_rows);
|
||||
|
||||
let total_w = CELL_W * 7.0 + PAD * 2.0;
|
||||
let total_h = HEADER_H + ROW_H * 0.75 + ROW_H * 6.0 + PAD * 2.0;
|
||||
View::<Msg>::new(Style {
|
||||
flex_direction: FlexDirection::Column,
|
||||
size: Size { width: length(total_w), height: length(total_h) },
|
||||
padding: llimphi_ui::llimphi_layout::taffy::Rect {
|
||||
top: length(PAD),
|
||||
bottom: length(PAD),
|
||||
left: length(PAD),
|
||||
right: length(PAD),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.fill(palette.bg)
|
||||
.radius(8.0)
|
||||
.children(vec![header, week_row, grid_box])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn days_in_month_meses_estandar() {
|
||||
assert_eq!(days_in_month(2026, 1), 31);
|
||||
assert_eq!(days_in_month(2026, 2), 28);
|
||||
assert_eq!(days_in_month(2024, 2), 29); // bisiesto
|
||||
assert_eq!(days_in_month(2026, 4), 30);
|
||||
assert_eq!(days_in_month(2026, 12), 31);
|
||||
// Fuera de rango → 0.
|
||||
assert_eq!(days_in_month(2026, 0), 0);
|
||||
assert_eq!(days_in_month(2026, 13), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_weekday_index_monday_y_sunday_start() {
|
||||
// 1 de enero 2026 cae JUEVES.
|
||||
// Con Monday-start: Mon=0, Tue=1, Wed=2, Thu=3 → index = 3.
|
||||
assert_eq!(first_weekday_index(2026, 1, WeekStart::Monday), 3);
|
||||
// Con Sunday-start: Sun=0, Mon=1, ..., Thu=4.
|
||||
assert_eq!(first_weekday_index(2026, 1, WeekStart::Sunday), 4);
|
||||
// 1 de febrero 2026 cae DOMINGO.
|
||||
// Monday-start: Sun=6.
|
||||
assert_eq!(first_weekday_index(2026, 2, WeekStart::Monday), 6);
|
||||
// Sunday-start: Sun=0.
|
||||
assert_eq!(first_weekday_index(2026, 2, WeekStart::Sunday), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn month_grid_enero_2026_monday_start() {
|
||||
// Enero 2026: 31 días, primer día jueves (index 3 con Monday-start).
|
||||
let g = month_grid(2026, 1, WeekStart::Monday);
|
||||
// Tres celdas vacías al inicio.
|
||||
assert_eq!(g[0][0], None);
|
||||
assert_eq!(g[0][1], None);
|
||||
assert_eq!(g[0][2], None);
|
||||
assert_eq!(g[0][3], Some(1)); // jueves 1
|
||||
assert_eq!(g[0][6], Some(4)); // domingo 4
|
||||
assert_eq!(g[1][0], Some(5)); // lunes 5
|
||||
// Último día: 31. (31 + 3 - 1) / 7 = 33/7 = 4, % 7 = 5 ⇒ fila 4, col 5.
|
||||
assert_eq!(g[4][5], Some(31));
|
||||
// Fila 5 (sexta) entera vacía.
|
||||
for c in g[5].iter() {
|
||||
assert_eq!(*c, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn month_grid_febrero_2026_28_dias_domingo_inicio() {
|
||||
// Febrero 2026: 28 días, primer día domingo (index 6 con Monday-start).
|
||||
let g = month_grid(2026, 2, WeekStart::Monday);
|
||||
// Primera fila: seis vacías + Some(1) en col 6 (domingo).
|
||||
for c in 0..6 {
|
||||
assert_eq!(g[0][c], None);
|
||||
}
|
||||
assert_eq!(g[0][6], Some(1));
|
||||
assert_eq!(g[1][0], Some(2)); // lunes 2
|
||||
// 28: (28+6-1)/7 = 33/7 = 4, %7 = 5 ⇒ fila 4 col 5.
|
||||
assert_eq!(g[4][5], Some(28));
|
||||
// Fila 5 entera vacía.
|
||||
for c in g[5].iter() {
|
||||
assert_eq!(*c, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_month_wrappea_anios() {
|
||||
// Enero → diciembre del año previo.
|
||||
assert_eq!(shift_month(2026, 1, -1), (2025, 12));
|
||||
// Diciembre → enero del año siguiente.
|
||||
assert_eq!(shift_month(2026, 12, 1), (2027, 1));
|
||||
// Salto de varios meses.
|
||||
assert_eq!(shift_month(2026, 6, -8), (2025, 10));
|
||||
assert_eq!(shift_month(2026, 6, 18), (2027, 12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn week_start_index_consistente() {
|
||||
// Monday-start.
|
||||
assert_eq!(WeekStart::Monday.index(Weekday::Mon), 0);
|
||||
assert_eq!(WeekStart::Monday.index(Weekday::Sun), 6);
|
||||
// Sunday-start.
|
||||
assert_eq!(WeekStart::Sunday.index(Weekday::Sun), 0);
|
||||
assert_eq!(WeekStart::Sunday.index(Weekday::Mon), 1);
|
||||
assert_eq!(WeekStart::Sunday.index(Weekday::Sat), 6);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user