package gw.util uses com.guidewire.pl.system.util.DateRange uses java.util.Date uses java.lang.StringBuilder uses java.util.Formatter uses java.util.concurrent.locks.ReentrantLock uses java.util.List /** * Utility class to draw a diagram of transactions and the costs they are related to, * like the following. THIS CLASS IS NOT THREAD SAFE. * *
 *                Cost #1 $77
 * 08/10/2010 |================| 11/10/2010
 *                   $77
 * 08/10/2010 |----------------| 11/10/2010
 *                    $-54
 *      09/07/2010 |-----------| 11/10/2010
 *
 *
 *               Cost #2 $125
 * 08/10/2010 |================| 11/10/2010
 *                   $125
 * 08/10/2010 |----------------| 11/10/2010
 *                     $-87
 *      09/07/2010 |-----------| 11/10/2010
 * 
 * 
*/ class TransactionGraph { // Width of the widest possible bar private var WIDTH : int = 73 // Indentation of the widest possible bar -- dates can be to the left of this margin private var PAD : int = 4 // used to set the destination for output private var output : block(s : String) = \ s -> {print(s)} // total number of days spanned by the list of transactions being graphed private var numDays : int // date range spanned by the list of transactions being graphed private var range : DateRange private var _sb : StringBuilder as Builder = new StringBuilder() private var _formatter : Formatter as Formatter = new Formatter(Builder) private var _showCostAmounts : boolean as ShowCostDetails = false private var _lock = new ReentrantLock() construct() {} /** * Construct a TransactionGraph that will draw with the specified total width. * barWidth is the width of the widest possible bar (one which spans the entire * date range). The actual maximum width of the chart could be as much as * 10 characters wider. */ construct(barWidth : int) { WIDTH = barWidth } /** * Construct a TransactionGraph that will draw with the specified total width. * barWidth is the width of the widest possible bar (one which spans the entire * date range). The actual maximum width of the chart could be at least (barWidth + indent) * and possibly several characters wider. */ construct(barWidth : int, indent : int) { WIDTH = barWidth PAD = indent } public function setOutput(doOutput : block(s : String)) : TransactionGraph { output = doOutput return this } private function clear() { Builder.setLength(0) } private function format(f : String, args : Object[]) { clear() Formatter.format(f, args) output(Formatter.toString()) } /** * Draw a diagram of a list of transactions, using the specified logger category. */ function drawDiagram(doOutput : block(s : String), transactions : List) { _lock.with(\-> { var tmp = output output = doOutput doDraw(transactions) output = tmp }) } /** * Draw a diagram of a list of transactions, using the logger category if set. */ function drawDiagram(transactions : List) { _lock.with(\-> { doDraw(transactions) }) } private function doDraw(transactions : List) { if (transactions.Empty) { output("") return } var bc = transactions.partition(\ txn -> txn.Cost) var costList = bc.Keys.toList().sortBy(\ cc -> cc.ID) var allDates = bc.Keys.toList().flatMap(\ cc -> { return {cc.EffDate, cc.ExpDate} }).toSet().toList().sort() range = DateRange.allDatesBetween(allDates.first(), allDates.last()) numDays = allDates.first().daysBetween(allDates.last()) for (cost in costList) { if (ShowCostDetails) { if (cost.StandardTermAmount != null) { format("(Standard) BaseRate = %6.2f, AdjustedRate = %6.2f, TermAmount = %6.2f for %d days", {cost.StandardBaseRate, cost.StandardAdjRate, cost.StandardTermAmount.Amount, cost.NumDaysInRatedTerm}) } if (cost.ActualTermAmount != cost.StandardTermAmount) { format("(Actual) BaseRate = %6.2f, AdjustedRate = %6.2f, TermAmount = %6.2f for %d days", {cost.ActualBaseRate, cost.ActualAdjRate, cost.ActualTermAmount.Amount, cost.NumDaysInRatedTerm}) } } printLabeledBar(cost.EffDate, cost.ExpDate, "${cost.DisplayName} ${cost.ActualAmount} / ${cost.ActualAmountBilling}", "=") // var txns = bc.get(cost).sort(\ a, b -> { return a.Amount > 0 or b.EffDate < a.EffDate } ) // onset first, then offsets from latest to earliest var txns = bc.get(cost).sortBy(\ c -> (c as KeyableBean).ID) txns.each(\ t -> printLabeledBar(t.EffDate, t.ExpDate, "${t.Amount} / ${t.AmountBilling}", "-")) output("") output("") } } private function printLabeledBar(start : Date, end : Date, label : String, barChar : String) { var startPoint = WIDTH*start.daysBetween(range.Start) / numDays var endPoint = WIDTH*end.daysBetween(range.Start) / numDays var chars = endPoint - startPoint var offset = PAD + startPoint if (offset > 11) { centerAt(offset + chars/2, label) printDatedBar(offset, chars, start, end, barChar) } else if (chars >= label.length + start.ShortFormat.length + 4) { var extra = chars - label.length - start.ShortFormat.length if (offset <= PAD) { format("%s%${extra/2}s%s%${extra/2}s%s", {start.ShortFormat, " ", label, " ", end.ShortFormat}) } else { format("%${offset - 4}s%s%${extra/2}s%s%${extra/2}s%s", {" ", start.ShortFormat, " ", label, " ", end.ShortFormat}) } printBar(offset, chars, barChar) } else { var endDateOffset = chars - end.ShortFormat.length if (endDateOffset < 1) endDateOffset = 1 if (offset <= 4) { format("%s%${endDateOffset}s%s\n", {start.ShortFormat, " ", end.ShortFormat}) } else { format("%${offset - 4}s%s%${endDateOffset}s%s", {" ", start.ShortFormat, " ", end.ShortFormat}) } printBar(offset, chars, barChar) centerAt(offset + chars/2, label) } } private function printDatedBar(offset : int, length : int, start : Date, end : Date, barChar : String) { clear() Formatter.format("%${offset - 11}s%s %s", {" ", start.ShortFormat, "|"}) for (1..|length) Formatter.out().append(barChar) Formatter.out().append("| ${end.ShortFormat}") output(Formatter.toString()) } private function printBar(offset : int, length : int, barChar : String) { clear() Formatter.format("%${offset}s%s", {" ", "|"}) for (1..|length) Formatter.out().append(barChar) Formatter.out().append("|") output(Formatter.toString()) } private function centerAt(column : int, message : String) { var offset = (column - message.length / 2) if (offset < 1) { output(message) } else { format("%${offset}s%s", {" ", message}) } } }