diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java index 9b007b55a6..a50eb23df4 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java @@ -158,6 +158,12 @@ public class Messages extends NLS public static String ColumnEntryValue; public static String ColumnErrorMessages; public static String ColumnExchangeRate; + public static String ColumnExpectedReturn; + public static String ColumnExpectedReturn_MenuLabel; + public static String ColumnExpectedReturn_Description; + public static String ColumnExpectedReturn_Tooltip_NotInUse; + public static String ColumnExpectedReturn_Tooltip_InUse; + public static String ColumnExpectedReturn_Tooltip_TotalPortfolioReturn; public static String ColumnExDate; public static String ColumnExitValue; public static String ColumnFeedURLHistoric; diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties index efc527a86a..024b1ac22a 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties @@ -357,6 +357,18 @@ ColumnExchangeRate = Exchange Rate ColumnExitValue = Exit value +ColumnExpectedReturn = Expected return + +ColumnExpectedReturn_Description = You may provide here your estimation of an expected return for some or all asset class and/or securities. From this, an estimated overall portfolio expected return is calculated (as weighted average) and displayed in the first row.\n\nPotential sources for return estimations can be e.g. historical returns of asset classes, yield to maturity for bonds or bond ETFs, estimates published for the current year by investment firms, or your own estimates. + +ColumnExpectedReturn_MenuLabel = Expected return + +ColumnExpectedReturn_Tooltip_InUse = This row is currently used in the calculation of overall portfolio expected returns. + +ColumnExpectedReturn_Tooltip_NotInUse = This row is currently not used in the calculation of overall portfolio expected returns. Double-click to edit. + +ColumnExpectedReturn_Tooltip_TotalPortfolioReturn = Calculated expected return of overall portfolio + ColumnFeedURLHistoric = URL (historic quotes) ColumnFeedURLLatest = URL (latest quotes) diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties index 7eb3ad2d75..d01b8bc0b7 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties @@ -355,6 +355,18 @@ ColumnExchangeRate = Wechselkurs ColumnExitValue = Ausstiegspreis +ColumnExpectedReturn = Erwartete Rendite + +ColumnExpectedReturn_Description = Sie k\u00F6nnen hier Ihre Sch\u00E4tzungen der (langfristig) erwarteten Rendite f\u00FCr Assetklassen und/oder einzelne Wertpapiere vergeben. Daraus wird eine erwartete Gesamtportfoliorendite (als gewichteter Durchschnitt) errechnet und in der Zeile "Asset Allocation" angezeigt.\n\nM\u00F6gliche Quellen f\u00FCr Sch\u00E4tzungen erwarteter Renditen:\n- Historische Renditen der Assetklassen (z.B. in Kommer, Ferri, ...)\n- Anleihen: Endf\u00E4lligkeitsrendite (YTM) minus Kosten f\u00FCr Anleihen-ETFs, oder Endf\u00E4lligkeitsrendite f\u00FCr Einzelanleihen\n- Sch\u00E4tzungen von Investmenth\u00E4usern f\u00FCr das jeweils aktuelle Jahr (basierend auf aktuellen Bewertungen) findet man mit Suchen wie z.B. "capital market assumptions for major asset classes 2019" (Achtung: Angaben sind oft nach Inflation, d.h. real statt nominal)\n- Eigene Sch\u00E4tzungen + +ColumnExpectedReturn_MenuLabel = Erwartete Rendite + +ColumnExpectedReturn_Tooltip_InUse = Diese Zeile wird bei der Berechnung der Gesamt-Portfoliorendite miteinbezogen. + +ColumnExpectedReturn_Tooltip_NotInUse = Diese Zeile wird derzeit nicht f\u00FCr die Berechnung der erwarteten Gesamtrenditen des Portfolios verwendet. Doppelklick zum bearbeiten. + +ColumnExpectedReturn_Tooltip_TotalPortfolioReturn = Errechnete erwartete Gesamtrendite des Portfolios + ColumnFeedURLHistoric = URL (historische Kurse) ColumnFeedURLLatest = URL (aktueller Kurs) diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_es.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_es.properties index 675df20e65..e232046c57 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_es.properties +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_es.properties @@ -309,6 +309,18 @@ ColumnExDate = Fecha de ejecuci\u00F3n ColumnExchangeRate = Tipo de cambio +ColumnExpectedReturn = Rendimiento esperado + +ColumnExpectedReturn_Description = Puede proporcionar aqu\u00ED su estimaci\u00F3n de un rendimiento esperado para algunos o todos los activos y / o valores. A partir de esto, se calcula un rendimiento esperado estimado de la cartera general (como promedio ponderado) y se muestra en la primera fila.\n\nLas fuentes potenciales para las estimaciones de rendimiento pueden ser rendimientos hist\u00F3ricos de clases de activos, rendimiento al vencimiento de bonos o ETF de bonos, estimaciones publicadas para el a\u00F1o en curso por empresas de inversi\u00F3n o sus propias estimaciones. + +ColumnExpectedReturn_MenuLabel = Rendimiento esperado + +ColumnExpectedReturn_Tooltip_InUse = Esta fila se utiliza actualmente en el c\u00E1lculo de los rendimientos esperados de la cartera general. + +ColumnExpectedReturn_Tooltip_NotInUse = Esta fila no se utiliza actualmente en el c\u00E1lculo de los rendimientos esperados de la cartera general. Doble click para editar. + +ColumnExpectedReturn_Tooltip_TotalPortfolioReturn = Rentabilidad esperada calculada de la cartera general + ColumnFeedURLHistoric = URL (cotizaciones hist\u00F3ricas) ColumnFeedURLLatest = URL (\u00FAltimas cotizaciones) diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_nl.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_nl.properties index 4065284197..e7728f7c63 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_nl.properties +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_nl.properties @@ -349,6 +349,18 @@ ColumnExchangeRate = Wisselkoers ColumnExitValue = Waarde afsluiten +ColumnExpectedReturn = Verwachte terugkomst + +ColumnExpectedReturn_Description = U kunt hier uw schatting geven van een verwacht rendement voor sommige of alle activaklassen en / of effecten. Hieruit wordt een geschat totaal verwacht rendement van de portefeuille berekend (als gewogen gemiddelde) en weergegeven in de eerste rij.\n\nPotenti\u00EBle bronnen voor rendementsschattingen kunnen historische rendementen van activaklassen, rendement tot looptijd van obligaties of obligatie-ETF's, schattingen die voor het lopende jaar door beleggingsondernemingen zijn gepubliceerd, of uw eigen schattingen zijn. + +ColumnExpectedReturn_MenuLabel = Verwachte terugkomst + +ColumnExpectedReturn_Tooltip_InUse = Deze rij wordt momenteel gebruikt bij de berekening van het verwachte verwachte rendement van de portefeuille. + +ColumnExpectedReturn_Tooltip_NotInUse = Deze rij wordt momenteel niet gebruikt bij de berekening van het verwachte verwachte rendement van de portefeuille. Dubbelklik om te bewerken. + +ColumnExpectedReturn_Tooltip_TotalPortfolioReturn = Berekend verwacht rendement van de totale portefeuille + ColumnFeedURLHistoric = URL (historische koersen) ColumnFeedURLLatest = URL (laatste koers) diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/StringToCurrencyConverter.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/StringToCurrencyConverter.java index e2b1a8e6cb..b012c686b4 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/StringToCurrencyConverter.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/StringToCurrencyConverter.java @@ -18,12 +18,25 @@ public class StringToCurrencyConverter implements IValidatingConverter type) + { + this(type, false); + } + + public StringToCurrencyConverter(Values type, boolean acceptNegativeValues) { this.factor = type.factor(); + StringBuilder patternString = new StringBuilder(); + patternString.append("^("); //$NON-NLS-1$ + + if (acceptNegativeValues) + patternString.append("-?"); //$NON-NLS-1$ + DecimalFormatSymbols symbols = new DecimalFormatSymbols(); - pattern = Pattern.compile("^([\\d" + symbols.getGroupingSeparator() + "]*)(" //$NON-NLS-1$ //$NON-NLS-2$ - + symbols.getDecimalSeparator() + "(\\d*))?$"); //$NON-NLS-1$ + patternString.append("[\\d").append(symbols.getGroupingSeparator()).append("]*)(") //$NON-NLS-1$ //$NON-NLS-2$ + .append(symbols.getDecimalSeparator()).append("(\\d*))?$"); //$NON-NLS-1$ + + pattern = Pattern.compile(patternString.toString()); full = new DecimalFormat("#,###"); //$NON-NLS-1$ } @@ -67,6 +80,7 @@ private long convertToLong(String part) throws ParseException String strBefore = m.group(1); Number before = strBefore.trim().length() > 0 ? full.parse(strBefore) : Long.valueOf(0); + boolean isNegative = strBefore.contains("-"); //$NON-NLS-1$ String strAfter = m.group(3); long after = 0; @@ -83,6 +97,7 @@ private long convertToLong(String part) throws ParseException after *= 10; } - return before.longValue() * factor + after; + // For negative numbers: subtract decimal digits instead of adding them + return before.longValue() * factor + (isNegative ? -after : after); } } diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/taxonomy/ExpectedReturnsAttachedModel.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/taxonomy/ExpectedReturnsAttachedModel.java new file mode 100644 index 0000000000..a88e7c373f --- /dev/null +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/taxonomy/ExpectedReturnsAttachedModel.java @@ -0,0 +1,363 @@ +package name.abuchen.portfolio.ui.views.taxonomy; + +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.StyledCellLabelProvider; +import org.eclipse.jface.viewers.StyledString; +import org.eclipse.jface.viewers.StyledString.Styler; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.TextStyle; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Text; + +import name.abuchen.portfolio.money.Values; +import name.abuchen.portfolio.ui.Messages; +import name.abuchen.portfolio.ui.util.NumberVerifyListener; +import name.abuchen.portfolio.ui.util.StringToCurrencyConverter; +import name.abuchen.portfolio.ui.util.viewers.Column; +import name.abuchen.portfolio.ui.util.viewers.ColumnEditingSupport; +import name.abuchen.portfolio.ui.util.viewers.ShowHideColumnHelper; + +public class ExpectedReturnsAttachedModel implements TaxonomyModel.AttachedModel +{ + public static final String KEY_EXPECTED_RETURN = "expected-return:value"; //$NON-NLS-1$ + public static final String KEY_ER_IN_USE = "expected-return:in-use"; //$NON-NLS-1$ + + private TaxonomyModel taxonomyModel; + + private int getExpectedReturnFor(TaxonomyNode node) + { + Integer expectedReturn = (Integer) node.getData(KEY_EXPECTED_RETURN); + return expectedReturn == null ? 0 : expectedReturn.intValue(); + } + + private void setExpectedReturnFor(TaxonomyNode node, int expectedReturn) + { + node.setData(KEY_EXPECTED_RETURN, Integer.valueOf(expectedReturn)); + } + + public boolean getIsERinUse(TaxonomyNode node) + { + Boolean inUse = (Boolean) node.getData(KEY_ER_IN_USE); + return Boolean.TRUE.equals(inUse); + } + + public void setIsERinUse(TaxonomyNode node, boolean isERinUse) + { + node.setData(KEY_ER_IN_USE, isERinUse); + } + + @Override + public void setup(TaxonomyModel model) + { + taxonomyModel = model; + } + + /** + * Method that is triggered when the user modifies a value in the "expected + * returns" column + */ + public void onERModified(Object element, Object newValue, Object oldValue) + { + TaxonomyNode node = (TaxonomyNode) element; + // Trigger recalculation of affected expected returns, in the model + recalcExpectedReturns(node); + + recalculate(this.taxonomyModel); + taxonomyModel.fireTaxonomyModelChange(node); + taxonomyModel.markDirty(); + } + + /** + * Adds a column where you can enter your expected return for this asset + * class (or security) + */ + @Override + public void addColumns(ShowHideColumnHelper columns) + { + Column column = new Column("expectedReturn", Messages.ColumnExpectedReturn, SWT.RIGHT, 100); //$NON-NLS-1$ + column.setMenuLabel(Messages.ColumnExpectedReturn_MenuLabel); + column.setDescription(Messages.ColumnExpectedReturn_Description); + column.setLabelProvider(new ExpectedReturnLabelProvider()); + + EREditingSupport eres = new EREditingSupport(); + eres.addListener(this::onERModified).attachTo(column); + + column.setSorter(null); + // Column is not visible by default, has to be added by user by using + // the menu + column.setVisible(false); + columns.addColumn(column); + + // Initial calculation of overall portfolio expected return. Calculate + // all expected returns from root downwards + calcFullERTree(taxonomyModel.getVirtualRootNode()); + } + + @Override + public void recalculate(TaxonomyModel model) + { + // Recalculate full expected returns tree + calcFullERTree(model.getVirtualRootNode()); + } + + /** + * Model calculations for expected returns feature. Recursively calculate + * the expected returns for the full ER (expected returns) tree below (and + * including) 'node'. This is used both upon init/update of the asset + * allocation page, and also after modifying an expected return field. + */ + public void calcFullERTree(TaxonomyNode node) + { + // Recurse over all children. Recursion will stop when node has no + // children, i.e. is leaf + node.getChildren().forEach(this::calcFullERTree); + + // If this node is an assignment (=a security), there is nothing to do - + // it either has an expected return assigned or not, + // but we don't need to calculate anything. And if this node is not in + // use, nothing needs to be done either. + if (node.isAssignment() || !getIsERinUse(node)) + return; + + // If one of the children is marked as unused, don't calculate this node + // either + for (TaxonomyNode child : node.getChildren()) + { + if (!getIsERinUse(child)) + return; + } + + // Finally, calc and update + // Rounding here seems to help in avoiding rounding errors + setExpectedReturnFor(node, (int) Math.round(calcERForNode(node, false))); + } + + /** + * Calculate the expected return for a given node, as a weighted average of + * the node's children's ER's. + * + * @param markAsUsed + * if true, mark this node's children as in use + */ + private double calcERForNode(TaxonomyNode parent, boolean markChildrenAsUsed) + { + double portfolioER = 0; + // Calculate parent's expected return as weighted average of expected + // returns of all + // siblings (all children of the parent) of the node. (Does not consider + // whether nodes are in use or not.) + for (TaxonomyNode node : parent.getChildren()) + { + // Divide amount in this asset class by amount of total assets (root + // of asset class tree) + long base = node.getParent() == null ? node.getActual().getAmount() + : node.getParent().getActual().getAmount(); + double pctOfCategory = node.getActual().getAmount() / (double) base; + portfolioER += pctOfCategory * getExpectedReturnFor(node); + + // Mark node as 'used' in portfolio + if (markChildrenAsUsed) + setIsERinUse(node, true); + } + return portfolioER; + } + + /** + * This is called after manually changing a ER field (i.e. from + * onERModified()). In order to make a calculation possible, mark the + * required nodes as "in use" or "not in use" and finally re-calculate the + * modified tree from the root downwards. + */ + public void recalcExpectedReturns(TaxonomyNode currentNode) + { + // Mark as in use: 1. this node and all its siblings, 2. all parents and + // their siblings up to root + markParentER(currentNode); + + // In the special situation where we have only one child, and that child + // is an assignment (=security), we can + // assign the new expected return to that child as well, since it + // logically must have the same ER + if (currentNode.getChildren().size() == 1 && currentNode.getChildren().get(0).isAssignment()) + { + // Set expected return of child to that of current node + setExpectedReturnFor(currentNode.getChildren().get(0), getExpectedReturnFor(currentNode)); + // and mark child as in use + setIsERinUse(currentNode.getChildren().get(0), true); + } + else + { + // Normal situation: + // Children below this modified node need to be marked as "not in + // use for the calculation of overall portfolio expected return". + // Mark as not in use: all children and their children (if I change + // ER of an asset class, obviously the assigned securities' ER + // cannot be used in the calculation anymore) + markChildrenAsERUnused(currentNode); + } + // Recalculate the full tree + calcFullERTree(currentNode); + } + + /** + * Recursively mark as in use: 1. this node and all its siblings, 2. all + * parents and their siblings up to root + */ + private void markParentER(TaxonomyNode currentNode) + { + TaxonomyNode parent = currentNode.getParent(); + for (TaxonomyNode node : parent.getChildren()) + setIsERinUse(node, true); + + setIsERinUse(parent, true); + + // Continue updating up the tree (further upwards until root) + if (!parent.isRoot()) + markParentER(parent); + } + + private void markChildrenAsERUnused(TaxonomyNode currentNode) + { + for (TaxonomyNode child : currentNode.getChildren()) + { + setIsERinUse(child, false); + if (child.getChildren() != null) + { + markChildrenAsERUnused(child); + } + } + } + + /** + * Implement ILabelProvider to enable CSV export to use + * {@link #getText(Object)}. + */ + private final class ExpectedReturnLabelProvider extends StyledCellLabelProvider implements ILabelProvider // NOSONAR + { + private Styler strikeoutStyler = new Styler() + { + @Override + public void applyStyles(TextStyle textStyle) + { + textStyle.strikeout = true; + textStyle.foreground = Display.getDefault().getSystemColor(SWT.COLOR_GRAY); + } + }; + + @Override + public void update(final ViewerCell cell) + { + TaxonomyNode node = (TaxonomyNode) cell.getElement(); + + String erText = Values.WeightPercent.format(getExpectedReturnFor(node)); + // If node is not in use, print percentage in grey and + // strikethrough + StyledString styledString = new StyledString(erText, getIsERinUse(node) ? null : strikeoutStyler); + cell.setText(styledString.getString()); + cell.setStyleRanges(styledString.getStyleRanges()); + } + + // This will still be used (for the CSV export) + @Override + public String getText(Object element) + { + TaxonomyNode node = (TaxonomyNode) element; + String prefix = getIsERinUse(node) ? "" : "(unused) "; //$NON-NLS-1$ //$NON-NLS-2$ + return prefix + Values.WeightPercent.format(getExpectedReturnFor(node)); + } + + @Override + public Image getImage(Object element) + { + return null; + } + + @Override + public String getToolTipText(Object element) + { + TaxonomyNode node = (TaxonomyNode) element; + + if (taxonomyModel.getClassificationRootNode().equals(node)) + return Messages.ColumnExpectedReturn_Tooltip_TotalPortfolioReturn; + + return getIsERinUse(node) ? Messages.ColumnExpectedReturn_Tooltip_InUse + : Messages.ColumnExpectedReturn_Tooltip_NotInUse; + } + + @Override + public Point getToolTipShift(Object object) + { + return new Point(0, 15); + } + } + + /** + * Class for column editor support. Differs from ValueEditingSupport in that + * it stores its value in a hash map in the TaxonomyNode with set/getData() + */ + class EREditingSupport extends ColumnEditingSupport + { + private StringToCurrencyConverter stringToLong; + + public EREditingSupport() + { + this.stringToLong = new StringToCurrencyConverter(Values.WeightPercent, true); + } + + @Override + public boolean canEdit(Object element) + { + return !taxonomyModel.getClassificationRootNode().equals(element); + } + + @Override + public CellEditor createEditor(Composite composite) + { + TextCellEditor textEditor = new TextCellEditor(composite); + ((Text) textEditor.getControl()).setTextLimit(20); + // 'true' in NumberVerifyListener() to allow negative values (for + // negative interest rates) + ((Text) textEditor.getControl()).addVerifyListener(new NumberVerifyListener(true)); + return textEditor; + } + + @Override + public Object getValue(Object element) throws Exception + { + TaxonomyNode node = (TaxonomyNode) element; + return Values.WeightPercent.format(getExpectedReturnFor(node)); + } + + @Override + public void setValue(Object element, Object value) throws Exception + { + TaxonomyNode node = (TaxonomyNode) element; + String str = String.valueOf(value); + + // If there is a trailing '%' in the user input, remove it so we can + // correctly convert it later on + if (str.length() > 0 && str.charAt(str.length() - 1) == '%') + str = str.substring(0, str.length() - 1); + Number newValue = stringToLong.convert(str); + newValue = Integer.valueOf(newValue.intValue()); + + Object oldValue = getExpectedReturnFor(node); + + // We do not check here whether oldValue == newValue because it may + // be desirable to update the field even if the newly entered value + // is the same as before (in order to switch the 'in use' flag) + if (newValue != null) + { + setExpectedReturnFor(node, (int) newValue); + setIsERinUse(node, true); + notify(element, newValue, oldValue); + } + } + } +} diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/taxonomy/TaxonomyModel.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/taxonomy/TaxonomyModel.java index c5f478363e..a0d0afea6a 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/taxonomy/TaxonomyModel.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/taxonomy/TaxonomyModel.java @@ -118,6 +118,7 @@ default void addColumns(ShowHideColumnHelper columns) this.snapshot = ClientSnapshot.create(client, converter, LocalDate.now()); this.attachedModels.add(new RecalculateTargetsAttachedModel()); + this.attachedModels.add(new ExpectedReturnsAttachedModel()); Classification virtualRoot = new Classification(null, Classification.VIRTUAL_ROOT, Messages.PerformanceChartLabelEntirePortfolio, taxonomy.getRoot().getColor()); diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/money/Values.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/money/Values.java index 264d8a432f..a0a89735fa 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/money/Values.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/money/Values.java @@ -258,6 +258,15 @@ public String format(Integer weight) } }; + public static final Values WeightPercent = new Values("#,##0.00", 100D, 100) //$NON-NLS-1$ + { + @Override + public String format(Integer weight) + { + return String.format("%,.2f%%", weight / divider()); //$NON-NLS-1$ + } + }; + public static final Values Percent2 = new Values("0.00%", 1D, 1) //$NON-NLS-1$ { @Override