Skip to content

Commit

Permalink
feat(review): split categories into (sub-)categories and deduplicated…
Browse files Browse the repository at this point in the history
… based on that
  • Loading branch information
simonsan committed Mar 8, 2024
1 parent caead71 commit b56ff07
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 15 deletions.
36 changes: 36 additions & 0 deletions crates/core/src/domain/category.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use serde_derive::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use ulid::Ulid;

use crate::GeneralConfig;

/// The category entity
#[derive(Debug, Serialize, Deserialize, TypedBuilder, Clone)]
pub struct Category {
Expand Down Expand Up @@ -56,6 +58,40 @@ pub fn extract_categories(category_string: &str, separator: &str) -> (Category,
}
}

/// Splits the category by the category separator or the default
/// separator from `GeneralConfig`
///
/// # Arguments
///
/// * `category_string` - The category string
/// * `separator` - The separator used to separate the category and subcategory
///
/// # Returns
///
/// A tuple containing the category and and optional subcategory
pub fn split_category_by_category_separator(
category_string: &str,
separator: Option<&str>,
) -> (String, Option<String>) {
let default_separator = GeneralConfig::default()
.category_separator()
.clone()
.unwrap_or("::".to_string());

let separator = separator.unwrap_or(default_separator.as_str());

let parts: Vec<_> = category_string.split(separator).collect();

if parts.len() > 1 {
// if there are more than one part, the first part is the category
// and the rest is the subcategory
(parts[0].to_string(), Some(parts[1..].concat()))
} else {
// if there is only one part, it's the category
(parts[0].to_string(), None)
}
}

/// The category id
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct CategoryGuid(Ulid);
Expand Down
8 changes: 4 additions & 4 deletions crates/core/src/domain/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ pub enum ReviewFormatKind {
/// Represents a category for summarizing activities.
// We use a string to allow for user-defined categories for now,
// but we may want to change this to an enum in the future.
pub type SummaryCategory = String;
pub type SummaryCategories = (String, String);

pub type SummaryGroupByCategory = BTreeMap<SummaryCategory, SummaryActivityGroup>;
pub type SummaryGroupByCategory = BTreeMap<SummaryCategories, SummaryActivityGroup>;

/// Represents a summary of activities and insights for a specified review period.
#[derive(
Expand Down Expand Up @@ -106,7 +106,7 @@ impl std::fmt::Display for ReviewSummary {
"Breaks (Amount)",
]);

for (category, summary_group) in self.summary_groups_by_category.iter() {
for ((category, subcategory), summary_group) in self.summary_groups_by_category.iter() {
builder.push_record(vec![
category,
"",
Expand All @@ -116,7 +116,7 @@ impl std::fmt::Display for ReviewSummary {

for (description, activity_group) in summary_group.activity_groups_by_description() {
builder.push_record(vec![
"",
subcategory,
description,
format!(
"{} ({})",
Expand Down
3 changes: 2 additions & 1 deletion crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ pub use crate::{
ActivityKindOptions, ActivitySession,
},
activity_log::ActivityLog,
category::split_category_by_category_separator,
filter::{ActivityFilterKind, FilteredActivities},
intermission::IntermissionAction,
review::{
Highlights, ReviewSummary, SummaryActivityGroup, SummaryCategory,
Highlights, ReviewSummary, SummaryActivityGroup, SummaryCategories,
SummaryGroupByCategory,
},
status::ActivityStatus,
Expand Down
38 changes: 29 additions & 9 deletions crates/core/src/service/activity_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{
commands::{resume::ResumeOptions, DeleteOptions, UpdateOptions},
domain::{
activity::{Activity, ActivityGuid, ActivityItem, ActivitySession},
category,
filter::{ActivityFilterKind, FilteredActivities},
review::SummaryGroupByCategory,
},
Expand Down Expand Up @@ -40,6 +41,12 @@ pub struct ActivityStoreCache {
by_start_date: BTreeMap<PaceDate, Vec<ActivityItem>>,
}

type Category = String;

type Subcategory = Option<String>;

type Description = String;

impl ActivityStore {
/// Create a new `ActivityStore` with a given storage backend
///
Expand Down Expand Up @@ -105,7 +112,7 @@ impl ActivityStore {
let mut summary_groups: SummaryGroupByCategory = BTreeMap::new();

let mut activity_sessions_lookup_by_category: HashMap<
(String, String),
(Category, Subcategory, Description),
Vec<ActivitySession>,
> = HashMap::new();

Expand All @@ -124,14 +131,21 @@ impl ActivityStore {
activity_session.add_multiple_intermissions(intermissions);
};

// Handle splitting subcategories
let (category, subcategory) = category::split_category_by_category_separator(
activity_item
.activity()
.category()
.as_deref()
.unwrap_or("Uncategorized"),
None,
);

// Deduplicate activities by category and description first
_ = activity_sessions_lookup_by_category
.entry((
activity_item
.activity()
.category()
.clone()
.unwrap_or("Uncategorized".to_string()),
category,
subcategory,
activity_item.activity().description().to_owned(),
))
.and_modify(|e| e.push(activity_session.clone()))
Expand All @@ -144,20 +158,26 @@ impl ActivityStore {
);

// Deduplicate activities by description
for ((category, description), activity_sessions) in &activity_sessions_lookup_by_category {
for ((category, subcategory, description), activity_sessions) in
&activity_sessions_lookup_by_category
{
if activity_sessions.is_empty() {
// Skip if there are no activity sessions
continue;
}

// Now we have a list of activity sessions grouped by description and category
// FIXME: This is a bit of a hack to handle the subcategory
// It will be an empty string if not present
let subcategory = subcategory.clone().unwrap_or_default();

// Now we have a list of activity sessions grouped by description and (sub)category
let activity_group = ActivityGroup::with_multiple_sessions(
description.clone(),
activity_sessions.to_vec(),
);

_ = summary_groups
.entry(category.clone())
.entry((category.clone(), subcategory))
.and_modify(|e| e.add_activity_group(activity_group.clone()))
.or_insert_with(|| SummaryActivityGroup::with_activity_group(activity_group));
}
Expand Down
2 changes: 1 addition & 1 deletion crates/core/tests/activity_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn test_activity_tracker(
);

let group = summary_groups_by_category
.get("development::pace")
.get(&("development".to_string(), "pace".to_string()))
.ok_or("Should have a category.")?;

assert_eq!(group.len(), 1, "Should have 1 activity.");
Expand Down

0 comments on commit b56ff07

Please sign in to comment.