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})
}
}
}