refactor: rewrite in rust
This commit is contained in:
parent
20a3e8b437
commit
ed493cff29
72 changed files with 5684 additions and 3688 deletions
84
src/timesheet/configuration.rs
Normal file
84
src/timesheet/configuration.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::models::{Dimension, Marker, MarkerPlacement, RepositoryConfiguration};
|
||||
|
||||
use super::TimesheetPointType;
|
||||
|
||||
pub const TIMESHEET_TAG: &str = "Timesheet";
|
||||
pub const TIMESHEET_DIMENSION_NAME: &str = "timesheet";
|
||||
|
||||
/// Pre-configured repository configuration for timesheet tracking.
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub static BasicTimesheetConfiguration: Lazy<RepositoryConfiguration> = Lazy::new(|| {
|
||||
RepositoryConfiguration::new()
|
||||
.with_dimension(
|
||||
TIMESHEET_DIMENSION_NAME,
|
||||
Dimension::new("Timesheet")
|
||||
.with_comment("Used by Timesheet-Subcommand to create Timecards")
|
||||
.with_propagate(false),
|
||||
)
|
||||
.with_marker(
|
||||
TIMESHEET_TAG,
|
||||
Marker::new("A default time card").with_placements(vec![MarkerPlacement::new(
|
||||
TIMESHEET_DIMENSION_NAME,
|
||||
)
|
||||
.with_value(TimesheetPointType::Card.as_str())
|
||||
.with_overwrites(false)]),
|
||||
)
|
||||
.with_marker(
|
||||
"VacationDay",
|
||||
Marker::new("Vacation Day").with_placements(vec![MarkerPlacement::new(
|
||||
TIMESHEET_DIMENSION_NAME,
|
||||
)
|
||||
.with_if_with(vec![TIMESHEET_TAG])
|
||||
.with_value(TimesheetPointType::Vacation.as_str())]),
|
||||
)
|
||||
.with_marker(
|
||||
"Break",
|
||||
Marker::new("Break").with_placements(vec![MarkerPlacement::new(
|
||||
TIMESHEET_DIMENSION_NAME,
|
||||
)
|
||||
.with_if_with(vec![TIMESHEET_TAG])
|
||||
.with_value(TimesheetPointType::Break.as_str())]),
|
||||
)
|
||||
.with_marker(
|
||||
"LunchBreak",
|
||||
Marker::new("Break").with_placements(vec![MarkerPlacement::new(
|
||||
TIMESHEET_DIMENSION_NAME,
|
||||
)
|
||||
.with_if_with(vec![TIMESHEET_TAG])
|
||||
.with_value(TimesheetPointType::Break.as_str())]),
|
||||
)
|
||||
.with_marker(
|
||||
"Feierabend",
|
||||
Marker::new("Break").with_placements(vec![MarkerPlacement::new(
|
||||
TIMESHEET_DIMENSION_NAME,
|
||||
)
|
||||
.with_if_with(vec![TIMESHEET_TAG])
|
||||
.with_value(TimesheetPointType::Break.as_str())]),
|
||||
)
|
||||
.with_marker(
|
||||
"Holiday",
|
||||
Marker::new("Official Holiday").with_placements(vec![MarkerPlacement::new(
|
||||
TIMESHEET_DIMENSION_NAME,
|
||||
)
|
||||
.with_if_with(vec![TIMESHEET_TAG])
|
||||
.with_value(TimesheetPointType::Holiday.as_str())]),
|
||||
)
|
||||
.with_marker(
|
||||
"SickLeave",
|
||||
Marker::new("Sick Leave").with_placements(vec![MarkerPlacement::new(
|
||||
TIMESHEET_DIMENSION_NAME,
|
||||
)
|
||||
.with_if_with(vec![TIMESHEET_TAG])
|
||||
.with_value(TimesheetPointType::SickLeave.as_str())]),
|
||||
)
|
||||
.with_marker(
|
||||
"UndertimeDay",
|
||||
Marker::new("Undertime Leave").with_placements(vec![MarkerPlacement::new(
|
||||
TIMESHEET_DIMENSION_NAME,
|
||||
)
|
||||
.with_if_with(vec![TIMESHEET_TAG])
|
||||
.with_value(TimesheetPointType::Undertime.as_str())]),
|
||||
)
|
||||
});
|
||||
537
src/timesheet/extract.rs
Normal file
537
src/timesheet/extract.rs
Normal file
|
|
@ -0,0 +1,537 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::error::StreamdError;
|
||||
use crate::models::{LocalizedShard, SpecialDayType, Timecard, Timesheet};
|
||||
use crate::query::find_shard_by_set_dimension;
|
||||
|
||||
use super::configuration::TIMESHEET_DIMENSION_NAME;
|
||||
use super::TimesheetPointType;
|
||||
|
||||
/// A point in time with an associated timesheet type.
|
||||
#[derive(Debug, Clone)]
|
||||
struct TimesheetPoint {
|
||||
moment: DateTime<Utc>,
|
||||
point_type: TimesheetPointType,
|
||||
}
|
||||
|
||||
/// Convert a localized shard to a timesheet point.
|
||||
fn shard_to_timesheet_point(shard: &LocalizedShard) -> Option<TimesheetPoint> {
|
||||
let type_str = shard.location.get(TIMESHEET_DIMENSION_NAME)?;
|
||||
let point_type = type_str.parse::<TimesheetPointType>().ok()?;
|
||||
|
||||
Some(TimesheetPoint {
|
||||
moment: shard.moment,
|
||||
point_type,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert localized shards to timesheet points.
|
||||
fn shards_to_timesheet_points(shards: &[LocalizedShard]) -> Vec<TimesheetPoint> {
|
||||
find_shard_by_set_dimension(shards, TIMESHEET_DIMENSION_NAME)
|
||||
.iter()
|
||||
.filter_map(shard_to_timesheet_point)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Aggregate timesheet points for a single day into a Timesheet.
|
||||
fn aggregate_timecard_day(points: &[TimesheetPoint]) -> Result<Option<Timesheet>, StreamdError> {
|
||||
if points.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let sorted_points: Vec<_> = {
|
||||
let mut pts = points.to_vec();
|
||||
pts.sort_by_key(|p| p.moment);
|
||||
pts
|
||||
};
|
||||
|
||||
let card_date = sorted_points[0].moment.date_naive();
|
||||
let mut is_sick_leave = false;
|
||||
let mut special_day_type: Option<SpecialDayType> = None;
|
||||
|
||||
// State machine: starting in "break" mode (not working)
|
||||
let mut last_is_break = true;
|
||||
let mut last_time = sorted_points[0].moment.time();
|
||||
let mut timecards: Vec<Timecard> = Vec::new();
|
||||
|
||||
for point in &sorted_points {
|
||||
if point.moment.date_naive() != card_date {
|
||||
return Err(StreamdError::TimesheetError(
|
||||
"Dates of all given timesheet days should be consistent".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let point_time = point.moment.time();
|
||||
|
||||
match point.point_type {
|
||||
TimesheetPointType::Holiday => {
|
||||
if special_day_type.is_some() {
|
||||
return Err(StreamdError::TimesheetError(format!(
|
||||
"{} is both {:?} and {:?}",
|
||||
card_date, point.point_type, special_day_type
|
||||
)));
|
||||
}
|
||||
special_day_type = Some(SpecialDayType::Holiday);
|
||||
}
|
||||
TimesheetPointType::Vacation => {
|
||||
if special_day_type.is_some() {
|
||||
return Err(StreamdError::TimesheetError(format!(
|
||||
"{} is both {:?} and {:?}",
|
||||
card_date, point.point_type, special_day_type
|
||||
)));
|
||||
}
|
||||
special_day_type = Some(SpecialDayType::Vacation);
|
||||
}
|
||||
TimesheetPointType::Undertime => {
|
||||
if special_day_type.is_some() {
|
||||
return Err(StreamdError::TimesheetError(format!(
|
||||
"{} is both {:?} and {:?}",
|
||||
card_date, point.point_type, special_day_type
|
||||
)));
|
||||
}
|
||||
special_day_type = Some(SpecialDayType::Undertime);
|
||||
}
|
||||
TimesheetPointType::SickLeave => {
|
||||
is_sick_leave = true;
|
||||
}
|
||||
TimesheetPointType::Break => {
|
||||
if !last_is_break {
|
||||
timecards.push(Timecard::new(last_time, point_time));
|
||||
last_is_break = true;
|
||||
last_time = point_time;
|
||||
}
|
||||
}
|
||||
TimesheetPointType::Card => {
|
||||
if last_is_break {
|
||||
last_is_break = false;
|
||||
last_time = point_time;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we ended in break mode
|
||||
if !last_is_break {
|
||||
return Err(StreamdError::TimesheetError(format!(
|
||||
"Last Timecard of {} is not a break!",
|
||||
card_date
|
||||
)));
|
||||
}
|
||||
|
||||
// Only return a timesheet if there's meaningful data
|
||||
if timecards.is_empty() && !is_sick_leave && special_day_type.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(Timesheet {
|
||||
date: card_date,
|
||||
is_sick_leave,
|
||||
special_day_type,
|
||||
timecards,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Aggregate timesheet points into timesheets, grouped by day.
|
||||
fn aggregate_timecards(points: &[TimesheetPoint]) -> Result<Vec<Timesheet>, StreamdError> {
|
||||
let mut timesheets = Vec::new();
|
||||
|
||||
// Group by date
|
||||
for (_date, group) in &points.iter().chunk_by(|p| p.moment.date_naive()) {
|
||||
let day_points: Vec<_> = group.cloned().collect();
|
||||
if let Some(timesheet) = aggregate_timecard_day(&day_points)? {
|
||||
timesheets.push(timesheet);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(timesheets)
|
||||
}
|
||||
|
||||
/// Extract timesheets from localized shards.
|
||||
pub fn extract_timesheets(shards: &[LocalizedShard]) -> Result<Vec<Timesheet>, StreamdError> {
|
||||
let points = shards_to_timesheet_points(shards);
|
||||
aggregate_timecards(&points)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::{NaiveTime, TimeZone};
|
||||
use indexmap::IndexMap;
|
||||
|
||||
fn point(at: DateTime<Utc>, point_type: TimesheetPointType) -> LocalizedShard {
|
||||
let mut location = IndexMap::new();
|
||||
location.insert(
|
||||
TIMESHEET_DIMENSION_NAME.to_string(),
|
||||
point_type.as_str().to_string(),
|
||||
);
|
||||
location.insert("file".to_string(), "dummy.md".to_string());
|
||||
|
||||
LocalizedShard {
|
||||
moment: at,
|
||||
markers: vec!["Timesheet".to_string()],
|
||||
tags: vec![],
|
||||
start_line: 1,
|
||||
end_line: 1,
|
||||
children: vec![],
|
||||
location,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_work_block() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(17, 30, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].date, day.date_naive());
|
||||
assert!(!result[0].is_sick_leave);
|
||||
assert!(result[0].special_day_type.is_none());
|
||||
assert_eq!(result[0].timecards.len(), 1);
|
||||
assert_eq!(
|
||||
result[0].timecards[0].from_time,
|
||||
NaiveTime::from_hms_opt(9, 0, 0).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
result[0].timecards[0].to_time,
|
||||
NaiveTime::from_hms_opt(17, 30, 0).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_three_work_blocks_separated_by_breaks() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(7, 15, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(12, 45, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(15, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(16, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(17, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].timecards.len(), 3);
|
||||
assert_eq!(
|
||||
result[0].timecards[0].from_time,
|
||||
NaiveTime::from_hms_opt(7, 15, 0).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
result[0].timecards[0].to_time,
|
||||
NaiveTime::from_hms_opt(12, 0, 0).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_input_order_is_not_required_within_a_day() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(15, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(7, 15, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(12, 45, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(17, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(16, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].timecards.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_groups_by_day() {
|
||||
let day1 = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let day2 = Utc.with_ymd_and_hms(2026, 2, 2, 0, 0, 0).unwrap();
|
||||
|
||||
let shards = vec![
|
||||
point(
|
||||
day1.with_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day1.with_time(NaiveTime::from_hms_opt(17, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
point(
|
||||
day2.with_time(NaiveTime::from_hms_opt(10, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day2.with_time(NaiveTime::from_hms_opt(18, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].date, day1.date_naive());
|
||||
assert_eq!(result[1].date, day2.date_naive());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_day_with_only_special_day_type_vacation() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(8, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Vacation,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].special_day_type, Some(SpecialDayType::Vacation));
|
||||
assert!(result[0].timecards.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_day_with_only_special_day_type_holiday() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(8, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Holiday,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].special_day_type, Some(SpecialDayType::Holiday));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_day_with_only_special_day_type_undertime() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(8, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Undertime,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].special_day_type, Some(SpecialDayType::Undertime));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_day_with_sick_leave_and_timecards() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(7, 30, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::SickLeave,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result[0].is_sick_leave);
|
||||
assert_eq!(result[0].timecards.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_day_with_sick_leave_only() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(8, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::SickLeave,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result[0].is_sick_leave);
|
||||
assert!(result[0].timecards.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_input() {
|
||||
let result = extract_timesheets(&[]).unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_day_with_only_cards_and_no_break_is_invalid() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Card,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards);
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.to_string().contains("not a break"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_special_day_types_same_day_is_invalid() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(8, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Vacation,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(8, 5, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Holiday,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(9, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards);
|
||||
|
||||
assert!(result.is_err());
|
||||
let err = result.unwrap_err();
|
||||
assert!(err.to_string().contains("is both"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_day_with_only_breaks_is_ignored() {
|
||||
let day = Utc.with_ymd_and_hms(2026, 2, 1, 0, 0, 0).unwrap();
|
||||
let shards = vec![
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(12, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
point(
|
||||
day.with_time(NaiveTime::from_hms_opt(13, 0, 0).unwrap())
|
||||
.unwrap(),
|
||||
TimesheetPointType::Break,
|
||||
),
|
||||
];
|
||||
|
||||
let result = extract_timesheets(&shards).unwrap();
|
||||
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
}
|
||||
7
src/timesheet/mod.rs
Normal file
7
src/timesheet/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
mod configuration;
|
||||
mod extract;
|
||||
mod point_types;
|
||||
|
||||
pub use configuration::{BasicTimesheetConfiguration, TIMESHEET_DIMENSION_NAME, TIMESHEET_TAG};
|
||||
pub use extract::extract_timesheets;
|
||||
pub use point_types::TimesheetPointType;
|
||||
54
src/timesheet/point_types.rs
Normal file
54
src/timesheet/point_types.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Type of timesheet point for time tracking.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TimesheetPointType {
|
||||
#[serde(rename = "CARD")]
|
||||
Card,
|
||||
#[serde(rename = "SICK_LEAVE")]
|
||||
SickLeave,
|
||||
#[serde(rename = "VACATION")]
|
||||
Vacation,
|
||||
#[serde(rename = "UNDERTIME")]
|
||||
Undertime,
|
||||
#[serde(rename = "HOLIDAY")]
|
||||
Holiday,
|
||||
#[serde(rename = "BREAK")]
|
||||
Break,
|
||||
}
|
||||
|
||||
impl TimesheetPointType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
TimesheetPointType::Card => "CARD",
|
||||
TimesheetPointType::SickLeave => "SICK_LEAVE",
|
||||
TimesheetPointType::Vacation => "VACATION",
|
||||
TimesheetPointType::Undertime => "UNDERTIME",
|
||||
TimesheetPointType::Holiday => "HOLIDAY",
|
||||
TimesheetPointType::Break => "BREAK",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TimesheetPointType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for TimesheetPointType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"CARD" => Ok(TimesheetPointType::Card),
|
||||
"SICK_LEAVE" => Ok(TimesheetPointType::SickLeave),
|
||||
"VACATION" => Ok(TimesheetPointType::Vacation),
|
||||
"UNDERTIME" => Ok(TimesheetPointType::Undertime),
|
||||
"HOLIDAY" => Ok(TimesheetPointType::Holiday),
|
||||
"BREAK" => Ok(TimesheetPointType::Break),
|
||||
_ => Err(format!("Unknown timesheet point type: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue