Move time-analyzer into it's own directory in preparation to merge into the main timestamper project.
This commit is contained in:
@ -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})" }
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.jdbernard.timeanalyzer.categories;
|
||||
|
||||
import com.jdbernard.timeanalyzer.events.Event;
|
||||
|
||||
public interface CategoryFilter {
|
||||
public boolean matchesEvent(Event event);
|
||||
}
|
@ -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() }
|
||||
|
||||
}
|
@ -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) } }
|
||||
|
||||
}
|
@ -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 }
|
||||
|
||||
}
|
@ -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) }
|
||||
}
|
@ -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) }
|
||||
}
|
@ -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) }
|
||||
}
|
@ -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)
|
||||
}
|
@ -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) } }
|
||||
}
|
@ -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 } }
|
||||
|
||||
}
|
@ -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]}:.*/ } }
|
||||
}
|
@ -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 }
|
||||
|
||||
}
|
@ -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})" }
|
||||
|
||||
}
|
@ -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) }
|
||||
}
|
@ -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}.*/ }
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user