/* 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.getString("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:List[String] = line.getString("dev").toList.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] = Bound(Date.now.floorDay, 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").toList.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 println("numTasks:" + taskSet.tasks.size) 200.ms.sleep }