
.10.dp.source-code.todo.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core_2.10 Show documentation
Show all versions of core_2.10 Show documentation
Core drx utilities that have no other dependencies other than the core scala lib
The newest version!
/*
Copyright 2010 Aaron J. Radke
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cc.drx
object Todo{
def main(args:Array[String]) = {
def isTodo(f:File):Boolean = {
f.isFile && f.ext == "kson" && {
val name = f.name.toLowerCase
name.contains("todo") && !name.contains("archive")
}
}
val todo = new Todo( for(arg <- args.toSeq; dir = File(arg).canon; f <- dir.walk if isTodo(f)) yield f )
}
}
class Todo(files:Seq[File]){
//---helper functions
val DatePat = """(\d+)/(\d+)""".r
def parseDate(date:String):Option[Date] = date match {
case DatePat(m,d) => Some( Date(Date.now.cal.y , m.toInt, d.toInt) )
case "today" | "now" => Some(Date.now.floorDay)
case _ => None
}
// def formatWorkDay(time:Time):String = f"~${time.hr/8/5}%2.1f [work-weeks]"
def formatWorkDay(time:Time):String = f"~${time.hr}%2.0fh"
private def split(str:String) = str split """[\s,;]+"""
object Task{
def get(line:KsonLine, index:Int):Option[Task] = {
val EstimateLoePat = """\[\s*\]\s*(\d+)\s*(h|hr|d|day|m|min)\s+(.+)""".r
val UnknownLoePat = """\[\s*\]\s*(.+)""".r
val (loe:Option[Time], desc:String) = line.root match {
case Some(EstimateLoePat(num, "h"|"hr",desc)) => (Some(num.toInt.hr), desc)
case Some(EstimateLoePat(num, "m"|"min",desc)) => (Some(num.toInt.minute), desc)
case Some(EstimateLoePat(num, "d"|"day",desc)) => (Some(8.hr*num.toInt), desc)
case Some(UnknownLoePat(desc)) => (None, desc)
case _ => (None, "Bad task string form: "+line.root)
}
val due:Option[Date] = line.getStringInherited("due").flatMap(parseDate)
due.map{d => Task(d, index, line, loe, desc)}
}
}
//tasks require a due date
case class Task(due:Date, index:Int, line:KsonLine, loe:Option[Time], desc:String){
lazy val scope:String = line.scope.flatMap{_.root}.reverse mkString "/"
lazy val parScope:String = line.scope.flatMap{_.root}.reverse.take(2) mkString "/"
lazy val devs:Seq[String] = line.getStringInherited("dev").toSeq.flatMap(split).sorted;
def sortTuple = (due.ms, index, scope, devs.mkString(",")) //Date object doesn't sort right inside the tuple ???
override def toString = summary
def summary = {
def format(time:Option[Time]):String = time.map{"%3dh" format _.hr.round.toInt} getOrElse "???h"
val loeString = format(loe)
val devString = if(devs.isEmpty) "No dev ???" else devs mkString ","
val budgetString = format(budget.map{_.time})
Seq(
'due -> due.usMonthDay.fit(10),
'loe -> s"$loeString/$budgetString".fit(10),
'dev -> devString.fit(20),
'scope -> scope,
'desc -> desc
).map(Format.kvColor).mkString(" ")
// f"${due.isoDay} $loeString/$budgetString ${scope}%-25s $devString%-15s $desc"
}
def loeDev(dev:String):Option[Time] = if(devs contains dev) loe.map{_/devs.size} else None //TODO split hours fractionally instead of evently
def budget:Option[Budget] = line.scope.flatMap{Budget.get}.headOption //search through parents for a budget grab the first
}
case class TaskSet(tasks:Seq[Task]){
lazy val loe:Time = tasks.flatMap(_.loe).foldLeft(0.s){_ + _}
lazy val due:Date = if(tasks.isEmpty) Date.now + 40.day else tasks.maxBy{_.due.ms}.due //TODO warn if no due date is set
lazy val span:Bound[Date] = Date.now.floorDay to due //TODO this date.now should not be hard coded...
// def budgetLoe:Time = tasks.flatMap(_.budgetLoe).foldLeft(0.s){_ + _}
//
lazy val taskTime = tasks.flatMap(_.loe).foldLeft(0.s){_ + _}
lazy val availableTime = workTime(span) //TODO select work day size from dev config
lazy val utilization:Double = if(availableTime <= 0.s) 1.0 else taskTime/availableTime
private def seekStart(end:Date, goal:Time, acc:Time):Date = {
if(acc >= goal) end
else seekStart( end - 1.day, goal, if(isWorkDay(end)) acc+8.hr else acc)
}
def start:Date = seekStart(span.end, taskTime, 0.s)
// def timeSummary = f"count:${tasks.size} tasks:${formatWorkDay(taskTime)} calendar:${formatWorkDay(availableTime)} utilization:${utilization}%2.2f"
def timeSummary = Seq( 'count -> tasks.size,
'tasks -> formatWorkDay(taskTime),
'calendar -> formatWorkDay(availableTime),
'utilization -> utilization,
'due -> due.usMonthDay,
'start -> start.usMonthDay,
'scope -> tasks.map{_.scope}.toSet.mkString(",")
).map(Format.kvColor(_)).mkString(" ")
// f"count:${tasks.size} tasks:${formatWorkDay(taskTime)} calendar:${formatWorkDay(availableTime)} utilization:${utilization}%2.2f"
}
// a `budget` is allow time from a given date (this may change based on new budget reports
object Budget{
private val BudgetPat = """(\d+)hr?s?\s*@\s*(\d+/\d+)""".r
def get(line:KsonLine):Option[Budget] = {
line.getString("budget") match {
case Some(BudgetPat(num, d)) => parseDate(d) map {Budget(num.toInt.hr, _, line)}
case _ => None
}
}
}
case class Budget(time:Time, start:Date, line:KsonLine){
val id = line.path
def nice = s"${time.hr}h @ ${start.isoDay}"
override def toString = s"$id Budget($nice)"
}
//-- find tasks
val kson = Kson(files)
val taskLines = for(line <- kson.lines; root <- line.root if root startsWith "[ ]") yield line
//-- find tasks
val dueTasks = for(
(line,index) <- taskLines.zipWithIndex;
task <- Task.get(line, index)
) yield task
//--find work dates while skipping ptos //TODO separate pto for each dev
val ptos = (
for(line <- kson.lines; pto <- line.getString("pto").toSeq.flatMap(split);
date <- parseDate(pto)
) yield date
).toSet
def isWorkDay(date:Date):Boolean = date.dayOfWeek.isWeekday && !ptos(date)
def workDates(bound:Bound[Date]) = bound by 1.day filter isWorkDay
def workTime(bound:Bound[Date], workDayLength:Time=8.hr):Time = workDayLength * workDates(bound).size
println("\n#=== Budgets\n")
//--all grouped by budget
val budgetTasks:Seq[Task] = {
for((budget,tasks) <- dueTasks.groupBy(_.budget)) yield {
val budgetString = budget map {_.toString} getOrElse "No budget defined"
println(s"\n#-- $budgetString\n")
val (estimatedTasks, unEstimatedTasks) = tasks.partition(_.loe.isDefined)
val loeSum = estimatedTasks.flatMap{_.loe}.foldLeft(0.s){_ + _}
val budgetTime = budget.map{_.time}.getOrElse(0.s)
val availableBudgetTime = budgetTime - loeSum
val avgTaskTime = availableBudgetTime/unEstimatedTasks.size //assume similar loe for remaining tasks if a loe is not defined
// for(task <- tasks.sortBy(_.sortTuple)) println(task)
//TODO squeeze/dialate time if over budget to a budgetedLoE
estimatedTasks ++ unEstimatedTasks.map{t => t.copy(loe=Some(avgTaskTime))}
}
}.toSeq.flatten.sortBy(_.sortTuple)
//--all sorted tasks (with auto filled budgets)
// budgetTasks foreach println
//
//--find specific *dev* tasks and re-budget tasks based specific *dev* and
//TODO make a way to see if this was an auto specified time or not
val devTasks = for(t <- budgetTasks; loe <- t.loeDev("radke")) yield t.copy(loe=Some(loe))
devTasks foreach println
//--grouped by parallel tasks
// println("\n#=== Parallel dates \n")
// for((due, tasks) <- devTasks groupBy (_.due) ) {
// println(s"#---$due")
// tasks foreach println
// }
println("\n#=== Parallel scope\n")
for((parScope, seqTasks) <- devTasks groupBy (_.parScope) ) {
println(s"#---$parScope")
// seqTasks.sortBy(_.sortTuple) foreach println
for((due, tasks) <- seqTasks.groupBy(_.due).toList sortBy {_._1.ms} ) {
val ts = TaskSet(tasks)
println(ts.timeSummary)
// tasks.sortBy(_.sortTuple) foreach println
}
// println(s"#---$due")
// tasks foreach println
}
val taskSets = for((parScope, seqTasks) <- devTasks groupBy (_.parScope);
(due, tasks) <- seqTasks.groupBy(_.due);
ts = TaskSet(tasks)
) yield ts
println("\n#=== Tasks by utilization pressure\n")
for(ts <- taskSets.toList.sortBy(_.utilization).reverse)
println(ts.timeSummary)
//--available work days
val taskSet = TaskSet(devTasks)
{
val cal = taskSet.span.max.cal
println(s"\n#=== Calendar now to ${cal.month.short} ${cal.d} ${taskSet.timeSummary}\n")
for(date <- workDates(taskSet.span)){ //TODO filter based on specific dev pto and available weekends
println( "## " + date.niceWork)
}
}
//--
//TODO schedule a task based on final due date via dynamic programming of due date stack serial by *dev*
//--schedule start dates based on available workDates
//TODO sort tasks into linear order
//TODO spread across due dates
// for(devTasks.reverse foreach println
// sort tasks??
//
//-- span and nudge scheduler..???
// optimize: by minimizing the amount of work at any given time while meeting deadlines
//
// [4 4 4 4 ]
// [2 2 2 2 2 2]
// =>
// [4 4 4 4 ]
// [1 1 1 1 4 4]
// =>
// [4 4 4 4 ] | a*4 = 4*4 = 16 => 4 0 0 = 16
// [1 1 0.5 0.5 4.5 4.5] | b*4 + c*2 = 6*2 = 12 0 4 2 12
// a + b = c => | a + b - c = 0 1 1 -1 0
//b*4 + c*2 = 12
//b+4 = c
// => b*4 + (b+4)*2 = 12
// => b*6 + 8 = 12
// => b*6 = 4
// => b = 4/6
//
// => b = 2/3
// => c = 2/3 + 4
// => a = 4
//
//
// [4 4 4 4 ] a*2 + b*2 + = 16 = [ 2 2 0 0] = 16
// [ 4 4 4 4] + c*2 + d*2 = 16 0 0 2 2 = 16
// => 1 -1 -1 0 = 0
// [5 5 3 3 ] a = b + c 1 0 0 -1 = 0
// [ 3 3 5 5] a = d
//
//
// [4 4 4 4 ]
// [ 2 2 2 2]
// =>
// [4 4 4 4 ]
// [ 0 0 4 4]
//
//first pass: statistic summary task/ and availability
//first pass: all serial alignment?
//--number of current tasks
Log(taskSet.tasks.size)
200.ms.sleep
}