package com.linkedin.thirdeye.dashboard.views.diffsummary;
import com.linkedin.thirdeye.client.diffsummary.DimNameValueCostEntry;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.linkedin.thirdeye.client.diffsummary.DimensionValues;
import com.linkedin.thirdeye.client.diffsummary.Dimensions;
import com.linkedin.thirdeye.client.diffsummary.HierarchyNode;
public class SummaryResponse {
private final static int MAX_GAINER_LOSER_COUNT = 5;
private final static NumberFormat DOUBLE_FORMATTER = new DecimalFormat("#0.00");
static final String INFINITE = "";
static final String ALL = "(ALL)";
static final String NOT_ALL = "(ALL)-";
static final String NOT_AVAILABLE = "-na-";
@JsonProperty("metricName")
private String metricName;
@JsonProperty("dimensions")
List<String> dimensions = new ArrayList<>();
@JsonProperty("responseRows")
private List<SummaryResponseRow> responseRows = new ArrayList<>();
@JsonProperty("gainer")
private List<SummaryGainerLoserResponseRow> gainer = new ArrayList<>();
@JsonProperty("loser")
private List<SummaryGainerLoserResponseRow> loser = new ArrayList<>();
private double totalBaselineValue = 0d;
private double totalCurrentValue = 0d;
public String getMetricName() {
return metricName;
}
public void setMetricName(String metricName) {
this.metricName = metricName;
}
public List<SummaryResponseRow> getResponseRows() {
return responseRows;
}
public List<SummaryGainerLoserResponseRow> getGainer() {
return gainer;
}
public List<SummaryGainerLoserResponseRow> getLoser() {
return loser;
}
public static SummaryResponse buildNotAvailableResponse() {
SummaryResponse response = new SummaryResponse();
response.dimensions.add(NOT_AVAILABLE);
// response.responseRows.add(SummaryResponseRow.buildNotAvailableRow());
return response;
}
private void buildGainerLoserGroup(List<DimNameValueCostEntry> costSet) {
for (DimNameValueCostEntry dimNameValueCostEntry : costSet) {
if (dimNameValueCostEntry.getCurrentValue() >= dimNameValueCostEntry.getBaselineValue() && gainer.size() < MAX_GAINER_LOSER_COUNT) {
gainer.add(buildGainerLoserRow(dimNameValueCostEntry));
} else if (dimNameValueCostEntry.getCurrentValue() < dimNameValueCostEntry.getBaselineValue() && loser.size() < MAX_GAINER_LOSER_COUNT) {
loser.add(buildGainerLoserRow(dimNameValueCostEntry));
}
if (gainer.size() >= MAX_GAINER_LOSER_COUNT && loser.size() >= MAX_GAINER_LOSER_COUNT) {
break;
}
}
}
private SummaryGainerLoserResponseRow buildGainerLoserRow(DimNameValueCostEntry costEntry) {
SummaryGainerLoserResponseRow row = new SummaryGainerLoserResponseRow();
row.baselineValue = costEntry.getBaselineValue();
row.currentValue = costEntry.getCurrentValue();
row.dimensionName = costEntry.getDimName();
row.dimensionValue = costEntry.getDimValue();
row.percentageChange = computePercentageChange(row.baselineValue, row.currentValue);
row.contributionChange =
computeContributionChange(row.baselineValue, row.currentValue, totalBaselineValue, totalCurrentValue);
row.contributionToOverallChange =
computeContributionToOverallChange(row.baselineValue, row.currentValue, totalBaselineValue);
row.cost = DOUBLE_FORMATTER.format(roundUp(costEntry.getCost()));
return row;
}
public void build(List<HierarchyNode> nodes, int targetLevelCount, List<DimNameValueCostEntry> costSet) {
// Compute the total baseline and current value
for(HierarchyNode node : nodes) {
totalBaselineValue += node.getBaselineValue();
totalCurrentValue += node.getCurrentValue();
}
this.buildGainerLoserGroup(costSet);
// If all nodes have a lower level count than targetLevelCount, then it is not necessary to print the summary with
// height higher than the available level.
int maxNodeLevelCount = 0;
for (HierarchyNode node : nodes) {
maxNodeLevelCount = Math.max(maxNodeLevelCount, node.getLevel());
}
targetLevelCount = Math.min(maxNodeLevelCount, targetLevelCount);
// Build the header
Dimensions dimensions = nodes.get(0).getDimensions();
for (int i = 0; i < targetLevelCount; ++i) {
this.dimensions.add(dimensions.get(i));
}
// Build the response
nodes = SummaryResponseTree.sortResponseTree(nodes, targetLevelCount);
// Build name tag for each row of responses
Map<HierarchyNode, NameTag> nameTags = new HashMap<>();
Map<HierarchyNode, List<String>> otherDimensionValues = new HashMap<>();
for (HierarchyNode node : nodes) {
NameTag tag = new NameTag(targetLevelCount);
nameTags.put(node, tag);
tag.copyNames(node.getDimensionValues());
otherDimensionValues.put(node, new ArrayList<String>());
}
// pre-condition: parent node is processed before its children nodes
for (HierarchyNode node : nodes) {
HierarchyNode parent = node;
int levelDiff = 1;
while ((parent = parent.getParent()) != null) {
NameTag parentNameTag = nameTags.get(parent);
if (parentNameTag != null) {
// Set tag from ALL to NOT_ALL String.
int notAllLevel = node.getLevel()-levelDiff;
parentNameTag.setNotAll(notAllLevel);
// For users' ease of understanding, we append what dimension values are excluded from NOT_ALL
StringBuilder sb = new StringBuilder();
String separator = "";
for (int i = notAllLevel; i < node.getDimensionValues().size(); ++i) {
sb.append(separator).append(node.getDimensionValues().get(i));
separator = ".";
}
otherDimensionValues.get(parent).add(sb.toString());
break;
}
++levelDiff;
}
}
// Fill in the information of each response row
for (HierarchyNode node : nodes) {
SummaryResponseRow row = new SummaryResponseRow();
row.names = nameTags.get(node).names;
row.baselineValue = node.getBaselineValue();
row.currentValue = node.getCurrentValue();
row.percentageChange = computePercentageChange(row.baselineValue, row.currentValue);
row.contributionChange =
computeContributionChange(row.baselineValue, row.currentValue, totalBaselineValue, totalCurrentValue);
row.contributionToOverallChange =
computeContributionToOverallChange(row.baselineValue, row.currentValue, totalBaselineValue);
StringBuilder sb = new StringBuilder();
String separator = "";
for (String s : otherDimensionValues.get(node)) {
sb.append(separator).append(s);
separator = ", ";
}
row.otherDimensionValues = sb.toString();
this.responseRows.add(row);
}
}
private static String computePercentageChange(double baseline, double current) {
if (baseline != 0d) {
double percentageChange = ((current - baseline) / baseline) * 100d;
return DOUBLE_FORMATTER.format(roundUp(percentageChange)) + "%";
} else {
return INFINITE;
}
}
private static String computeContributionChange(double baseline, double current, double totalBaseline, double totalCurrent) {
if (totalCurrent != 0d && totalBaseline != 0d) {
double contributionChange = ((current / totalCurrent) - (baseline / totalBaseline)) * 100d;
return DOUBLE_FORMATTER.format(roundUp(contributionChange)) + "%";
} else {
return INFINITE;
}
}
private static String computeContributionToOverallChange(double baseline, double current, double totalBaseline) {
if (totalBaseline != 0d) {
double contributionToOverallChange = ((current - baseline) / (totalBaseline)) * 100d;
return DOUBLE_FORMATTER.format(roundUp(contributionToOverallChange)) + "%";
} else {
return INFINITE;
}
}
private static double roundUp(double number) {
return Math.round(number * 100d) / 100d;
}
public String toString() {
ToStringBuilder tsb = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
tsb.append('\n').append(this.dimensions);
for (SummaryResponseRow row : getResponseRows()) {
tsb.append('\n').append(row);
}
return tsb.toString();
}
private static class NameTag {
private List<String> names;
public NameTag(int levelCount) {
names = new ArrayList<>(levelCount);
for (int i = 0; i < levelCount; ++i) {
names.add(ALL);
}
}
public void copyNames(DimensionValues dimensionValues) {
for (int i = 0; i < dimensionValues.size(); ++i) {
names.set(i, dimensionValues.get(i));
}
}
public void setNotAll(int index) {
names.set(index, NOT_ALL);
}
}
}