Move time-analyzer into it's own directory in preparation to merge into the main timestamper project.

This commit is contained in:
2024-08-04 21:22:38 -05:00
parent 00a5252542
commit ea3f554224
41 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,121 @@
package com.jdbernard.timeanalyzer.categories
import com.jdbernard.timeanalyzer.events.Event
import org.joda.time.Duration
import org.joda.time.format.PeriodFormat
import org.joda.time.format.PeriodFormatter
/**
* A `Category` represents a collection of like `Events` and sub-categories.
*/
public abstract class Category implements Comparable<Category> {
public static PeriodFormatter periodFormatter = PeriodFormat.getDefault()
/** List of events directly under this category. */
public List events
/** List of sub-categories under this category. */
public List categories
/** List of `CategorizationPlan`s to use when adding new `Event`s to the
* category. */
public List categorizationPlans
/** A end-user-friendly text description of the category.*/
public String description
public Category() { this("Unamed Category") }
public Category(String description) {
events = []
categories = []
categorizationPlans = []
this.description = description }
/**
* Does the given event belong in this category?
* @param e `Event` being considered.
* @return **`true`** if this event belongs in this category, **`false`**
* otherwise.
*/
public abstract boolean matchesEvent(Event e)
/**
* Add an event to this category. This method does not check to see if the
* `Event` matches this category. It assumed that the check has already been
* made and the `Event` matches.*/
public Event addEvent(Event event) {
def addedEvent
// Try first to add it to a subcategory (or create a new subcategory).
addedEvent = addToSubcategory(event)
// Cannot add to a subcategory, add to ourselves.
if (!addedEvent) {
events << event
addedEvent = event }
return addedEvent }
public Event addToSubcategory(Event e) {
// find all matching subcategories
def matchingCategories = categories.findAll { it.matchesEvent(e) }
if (matchingCategories) {
return matchingCategories[0].addEvent(e) }
// no matching subcategories, can we create a new one based on one
// of our plans?
def matchingPlans = categorizationPlans.findAll {
it.deservesNewCategory(e, events) }
if (matchingPlans) {
// create the new category
def newCategory = matchingPlans[0].newCategory(e, events)
// add it to our list of cateogries
categories << newCategory
// add the new event to the category
def addedEvent = newCategory.addEvent(e)
// move all the events that match the new category over
def existingEvents = matchingPlans[0].findEventsToRecategorize(e, events)
events -= existingEvents
existingEvents.each { newCategory.addEvent(it) }
// return the new entry
return addedEvent }
return null }
public Category filter(List<CategoryFilter> filters) {
// create new filtered category
FilteredCategory fc = new FilteredCategory(description)
fc.filters = filters
// filter all events and add them to the category
fc.events
// TODO
}
public Category filter(CategoryFilter filter) { return filter([filter]) }
public Category filter(Closure c) { return filter(c as CategoryFilter) }
public Duration getDuration() {
return categories.sum(new Duration(0)) { it.duration } +
events.sum(new Duration(0)) { it.duration } }
public int compareTo(Category other) {
return this.getDuration().compareTo(other.getDuration()) }
public String toString() {
String period = periodFormatter.print(this.duration.toPeriod())
return "${description} (${period})" }
}

View File

@ -0,0 +1,7 @@
package com.jdbernard.timeanalyzer.categories;
import com.jdbernard.timeanalyzer.events.Event;
public interface CategoryFilter {
public boolean matchesEvent(Event event);
}

View File

@ -0,0 +1,15 @@
package com.jdbernard.timeanalyzer.categories
import com.jdbernard.timeanalyzer.events.Event
public class DescriptionBasedCategory extends Category {
public DescriptionBasedCategory(String description) {
super()
this.description = description.replaceAll(/\p{Punct}/, '') }
public boolean matchesEvent(Event e) {
return e.description.replaceAll(/\p{Punct}/, '').toLowerCase() ==
description.toLowerCase() }
}

View File

@ -0,0 +1,15 @@
package com.jdbernard.timeanalyzer.categories
import com.jdbernard.timeanalyzer.events.Event
public class FilteredCategory extends Category {
List<CategoryFilter> filters = []
public FilteredCategory(String description) {
super(description) }
public boolean matchesEvent(Event e) {
return filters.every { filter -> filter.matchesEvent(e) } }
}

View File

@ -0,0 +1,13 @@
package com.jdbernard.timeanalyzer.categories
import com.jdbernard.timeanalyzer.events.Event
public class GeneralCategory extends Category {
public GeneralCategory() { this("General") }
public GeneralCategory(String description) { super(description) }
public boolean matchesEvent(Event e) { true }
}

View File

@ -0,0 +1,18 @@
package com.jdbernard.timeanalyzer.categories
import com.jdbernard.timeanalyzer.events.Event
import org.joda.time.Interval
public class TimeIntervalCategory extends Category {
private final Interval interval
public TimeIntervalCategory(String desc, Interval interval) {
super(desc)
this.interval = interval }
public boolean matchesEvent(Event e) {
Interval eventIv = new Interval(e.start, e.duration)
return interval.contains(eventIv) }
}

View File

@ -0,0 +1,22 @@
package com.jdbernard.timeanalyzer.categories
import com.jdbernard.timeanalyzer.events.Event
import org.joda.time.Interval
import org.joda.time.ReadableInstant
import org.joda.time.ReadableInterval
public class TimeIntervalCategoryFilter implements CategoryFilter {
ReadableInterval interval
public TimeIntervalCategoryFilter(ReadableInterval interval) {
this.interval = interval }
public TimeIntervalCategoryFilter(ReadableInstant start,
ReadableInstant end) {
this.interval = new Interval(start, end) }
public boolean matchesEvent(Event event) {
return interval.contains(event.start) }
}

View File

@ -0,0 +1,23 @@
package com.jdbernard.timeanalyzer.categories
import com.jdbernard.timeanalyzer.events.Event
public class TwoLevelCategory extends Category {
private final def descriptionPattern
public TwoLevelCategory(String heading) {
super(heading)
descriptionPattern = ~/^${heading}:\s*(.*)/ }
public boolean matchesEvent(Event e) {
return e.description ==~ descriptionPattern }
public Event addEvent(Event e) {
def m = e.description =~ descriptionPattern
e = new Event(e, description: m[0][1])
super.addEvent(e) }
}

View File

@ -0,0 +1,24 @@
package com.jdbernard.timeanalyzer.categorizationplans;
import com.jdbernard.timeanalyzer.categories.Category;
import com.jdbernard.timeanalyzer.events.Event;
import java.util.List;
import java.util.Map;
public abstract class CategorizationPlan {
public CategorizationPlan() {}
public CategorizationPlan(Closure newCatSetupFun) {
newCategorySetupFun = newCatSetupFun }
protected Closure newCategorySetupFun
protected void setupNewCategory(Category cat) {
if (newCategorySetupFun) newCategorySetupFun(cat) }
public abstract boolean deservesNewCategory(Event event, List<Event> existingEvents)
public abstract Category newCategory(Event event, List<Event> existingEvents)
public abstract List<Event> findEventsToRecategorize(Event event, List<Event> existingEvents)
}

View File

@ -0,0 +1,44 @@
package com.jdbernard.timeanalyzer.categorizationplans
import com.jdbernard.timeanalyzer.categories.Category
import com.jdbernard.timeanalyzer.categories.TimeIntervalCategory
import com.jdbernard.timeanalyzer.events.Event
import org.joda.time.DateTime
import org.joda.time.Interval
import org.joda.time.Period
public class DailyCategorizationPlan extends CategorizationPlan {
public DailyCategorizationPlan() {}
public DailyCategorizationPlan(Closure newCatSetupFun) {
super(newCatSetupFun) }
boolean deservesNewCategory(Event event, List<Event> existingEvents) {
Interval fullDay = new Interval(
event.start.toDateMidnight(), Period.days(1))
Interval eventIv = new Interval(
event.start, event.duration)
return fullDay.contains(eventIv) }
Category newCategory(Event event, List<Event> existingEvents) {
Interval fullday = new Interval(
event.start.toDateMidnight(), Period.days(1))
Category newCat = new TimeIntervalCategory(
event.start.toString("EEE, MMM dd"), fullday)
setupNewCategory(newCat)
return newCat }
List<Event> findEventsToRecategorize(Event event,
List<Event> existingEvents) {
Interval fullday = new Interval(
event.start.toDateMidnight(), Period.days(1))
return existingEvents.findAll {
Interval iv = new Interval(it.start, it.duration)
fullday.contains(iv) } }
}

View File

@ -0,0 +1,30 @@
package com.jdbernard.timeanalyzer.categorizationplans
import com.jdbernard.timeanalyzer.categories.Category
import com.jdbernard.timeanalyzer.categories.DescriptionBasedCategory
import com.jdbernard.timeanalyzer.events.Event
public class DescriptionBasedCategorizationPlan extends CategorizationPlan {
public DescriptionBasedCategorizationPlan() {}
public DescriptionBasedCategorizationPlan(Closure newCatSetupFun) {
super(newCatSetupFun) }
public boolean deservesNewCategory(Event event, List<Event> existingEvents) {
def desc = event.description.replaceAll(/\p{Punct}/, '').toLowerCase()
return existingEvents.any {
it.description.replaceAll(/\p{Punct}/, '').toLowerCase() == desc } }
public Category newCategory(Event event,
List<Event> existingEvents) {
def newCat = new DescriptionBasedCategory(event.description)
setupNewCategory(newCat)
return newCat }
public List<Event> findEventsToRecategorize(Event event,
List<Event> existingEvents) {
def desc = event.description.replaceAll(/\p{Punct}/, '').toLowerCase()
return existingEvents.findAll {
it.description.replaceAll(/\p{Punct}/, '').toLowerCase() == desc } }
}

View File

@ -0,0 +1,28 @@
package com.jdbernard.timeanalyzer.categorizationplans
import com.jdbernard.timeanalyzer.categories.Category
import com.jdbernard.timeanalyzer.categories.TwoLevelCategory
import com.jdbernard.timeanalyzer.events.Event
public class TwoLevelCategorizationPlan extends CategorizationPlan {
private static final def TWO_LEVEL_PATTERN = ~/(.+?):(.*)/
public TwoLevelCategorizationPlan() {}
public TwoLevelCategorizationPlan(Closure newCatSetupFun) {
super(newCatSetupFun) }
public boolean deservesNewCategory(Event event, List<Event> el) {
return event ==~ TWO_LEVEL_PATTERN }
public Category newCategory(Event event, List<Event> el) {
def m = event.description =~ TWO_LEVEL_PATTERN
def newCat = new TwoLevelCategory(m[0][1])
setupNewCategory(newCat)
return newCat }
public List<Event> findEventsToRecategorize(Event event,
List<Event> existingEvents) {
def m = event.description =~ TWO_LEVEL_PATTERN
return existingEvents.findAll { it.description ==~ /${m[0][1]}:.*/ } }
}

View File

@ -0,0 +1,19 @@
package com.jdbernard.timeanalyzer.chart
import com.jdbernard.timeanalyzer.categories.Category
import org.jfree.data.general.DefaultPieDataset
import org.jfree.data.general.PieDataset
import org.jfree.util.SortOrder
public class Util {
public static PieDataset makePieDataset(Category category) {
DefaultPieDataset dpds = new DefaultPieDataset()
category.categories.each { cat ->
dpds.setValue(cat.description, cat.duration.standardSeconds) }
category.events.each { entry ->
dpds.setValue(entry.description, entry.duration.standardSeconds) }
dpds.sortByValues(SortOrder.DESCENDING)
return dpds }
}

View File

@ -0,0 +1,40 @@
package com.jdbernard.timeanalyzer.events
import org.joda.time.ReadableDateTime
import org.joda.time.Duration
import org.joda.time.format.PeriodFormat
import org.joda.time.format.PeriodFormatter
public class Event implements Cloneable {
public static PeriodFormatter periodFormatter = PeriodFormat.getDefault()
public final String description
public final String notes
public final ReadableDateTime start
public Duration duration // should be final, allows modification for the
// TimelineEventProcessor
public Event(String desc, String notes, ReadableDateTime start, Duration duration) {
this.description = desc
this.notes = notes
this.start = start
this.duration = duration }
public Event(Map params) {
this.description = params.description ?: ""
this.notes = params.notes ?: ""
this.start = params.start
this.duration = params.duration }
public Event(Map params, Event e) {
this.description = params.description ?: e.description
this.notes = params.notes ?: e.notes
this.start = params.start ?: e.start
this.duration = params.duration ?: e.duration }
public String toString() {
String period = periodFormatter.print(this.duration.toPeriod())
return "${description} (${period})" }
}

View File

@ -0,0 +1,51 @@
package com.jdbernard.timeanalyzer.processors
import com.jdbernard.timeanalyzer.events.Event
import com.jdbernard.timestamper.core.Timeline
import com.jdbernard.timestamper.core.TimelineProperties
import org.joda.time.DateTime
import org.joda.time.Duration
public class TimelineEventProcessor {
/** Events whose description matches one of these regex strings or
* patterns are ignored. */
List exclusions = []
public TimelineEventProcessor() {}
public TimelineEventProcessor(List exclusions) { this.exclusions = exclusions }
public List<Event> process(File timelinePropFile) {
def timelineProps = new TimelineProperties(timelinePropFile)
return process(timelineProps.timeline) }
public List<Event> process(Timeline timeline) {
List<Event> events = []
timeline.each { marker ->
Event e = new Event(
description: marker.mark,
notes: marker.notes,
start: new DateTime(marker.timestamp),
duration: new Duration(0))
println e
// if this is not the first event, then we need to update the
// duration of the previous event
if (events.size > 0) {
Event lastEvent = events[-1]
lastEvent.duration = new Duration(lastEvent.start, e.start) }
events << e }
def excluded = events.findAll { event ->
exclusions.any { exclusion -> event.description ==~ exclusion } }
return events - excluded }
public static List<Event> process(def timeline, List exclusions) {
def inst = new TimelineEventProcessor(exclusions)
return inst.process(timeline) }
}

View File

@ -0,0 +1,33 @@
package com.quantumdigital.ithelp.timeanalyzer
import com.jdbernard.timeanalyzer.categories.Category
import com.jdbernard.timeanalyzer.categorizationplans.CategorizationPlan
import com.jdbernard.timeanalyzer.events.Event
public class TicketCategorizationPlan extends CategorizationPlan {
private static def TICKET_PATTERN = ~/.*#(\d+).*/
public boolean deservesNewCategory(Event e, List<Event> el) {
return e.description ==~ TICKET_PATTERN
}
public Category newCategory(Event e, List<Event> el) {
def m = e.description =~ TICKET_PATTERN
def newCat = new TicketCategory(m[0][1] as int)
setupNewCategory(newCat)
return newCat
}
public List<Event> findEventsToRecategorize(Event e,
List<Event> existingEvents) {
def m = e.description =~ TICKET_PATTERN
int ticketId = m[0][1] as int
return existingEvents.findAll {
it.description ==~ /.*#${ticketId}.*/ }
}
}

View File

@ -0,0 +1,25 @@
package com.quantumdigital.ithelp.timeanalyzer
import com.jdbernard.timeanalyzer.categories.Category
import com.jdbernard.timeanalyzer.events.Event
public class TicketCategory extends Category {
public final int ticketId
public TicketCategory(int ticketId) {
super()
this.ticketId = ticketId
this.description = "Ticket #${ticketId}"
}
public boolean matchesEvent(Event e) {
return (e.description ==~ /.*#${ticketId}.*/)
}
public Event addEvent(Event e) {
TicketEvent te = new TicketEvent(e)
events << te
return te
}
}

View File

@ -0,0 +1,37 @@
package com.quantumdigital.ithelp.timeanalyzer
import com.jdbernard.timeanalyzer.events.Event
public class TicketEvent extends Event {
public final int id
public TicketEvent(String desc, String notes, String start, String duration) {
super(desc, notes, start, duration)
def m = desc =~ /.*#(\d+).*/
this.id = m[0][1] as int
}
public TicketEvent(Map params) {
super(params)
def m = description =~ /.*#(\d+).*/
this.id = m[0][1] as int
}
public TicketEvent(Map params, Event e) {
super(params, e)
def m = description =~ /.*#(\d+).*/
this.id = m[0][1] as int
}
public TicketEvent(Event e) {
super([:], e)
def m = description =~ /.*#(\d+).*/
this.id = m[0][1] as int
}
}