diff --git a/CHANGELOG.md b/CHANGELOG.md index e497a45..ccfd996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # KdbInsideBrains Changelog +## [5.4.0] + +### Added + +- Issue #91: Support for scientific notation in table output (disabled by default. Can be enabled in the Settings) +- Decimal Rounding Mode added with Half-Even by default as before (can be changed in the Settings) + +### Changed + +- Numerical Format settings moved into a separate configuration section in the Settings + ## [5.3.0] ### Added diff --git a/src/main/java/icons/KdbIcons.java b/src/main/java/icons/KdbIcons.java index de2f6b1..9a9872f 100644 --- a/src/main/java/icons/KdbIcons.java +++ b/src/main/java/icons/KdbIcons.java @@ -92,6 +92,7 @@ public static final class Console { public static final @NotNull Icon FlipTable = load("/org/kdb/inside/brains/icons/console/flipTable.svg"); public static final @NotNull Icon TableIndex = load("/org/kdb/inside/brains/icons/console/tableIndex.svg"); public static final @NotNull Icon TableThousands = load("/org/kdb/inside/brains/icons/console/thousandsSeparator.svg"); + public static final @NotNull Icon TableScientific = load("/org/kdb/inside/brains/icons/console/scientificNotation.svg"); public static final @NotNull Icon SelectAll = AllIcons.Actions.Selectall; public static final @NotNull Icon UnselectAll = AllIcons.Actions.Unselectall; diff --git a/src/main/java/org/kdb/inside/brains/settings/KdbSettingsConfigurable.java b/src/main/java/org/kdb/inside/brains/settings/KdbSettingsConfigurable.java index 2b16228..b7aef31 100644 --- a/src/main/java/org/kdb/inside/brains/settings/KdbSettingsConfigurable.java +++ b/src/main/java/org/kdb/inside/brains/settings/KdbSettingsConfigurable.java @@ -6,6 +6,7 @@ import org.jetbrains.annotations.Nullable; import org.kdb.inside.brains.core.ExecutionOptionsPanel; import org.kdb.inside.brains.view.console.ConsoleOptionsPanel; +import org.kdb.inside.brains.view.console.NumericalOptionsPanel; import org.kdb.inside.brains.view.console.TableOptionsPanel; import org.kdb.inside.brains.view.inspector.InspectorOptionsPanel; @@ -13,6 +14,7 @@ public class KdbSettingsConfigurable extends KdbConfigurable { private final TableOptionsPanel tableOptionsPanel = new TableOptionsPanel(); + private final NumericalOptionsPanel numericalOptionsPanel = new NumericalOptionsPanel(); private final ConsoleOptionsPanel consoleOptionsPanel = new ConsoleOptionsPanel(); private final ExecutionOptionsPanel executionOptionsPanel = new ExecutionOptionsPanel(); private final InspectorOptionsPanel inspectorOptionsPanel = new InspectorOptionsPanel(); @@ -35,7 +37,12 @@ public JComponent createComponent() { .addComponent(consoleOptionsPanel) .setFormLeftIndent(0) - .addComponent(new TitledSeparator("Table View Options")) + .addComponent(new TitledSeparator("Numerical Format")) + .setFormLeftIndent(FORM_LEFT_INDENT) + .addComponent(numericalOptionsPanel) + + .setFormLeftIndent(0) + .addComponent(new TitledSeparator("Table Options")) .setFormLeftIndent(FORM_LEFT_INDENT) .addComponent(tableOptionsPanel) @@ -60,6 +67,10 @@ public boolean isModified() { return true; } + if (!Comparing.equal(settingsService.getNumericalOptions(), numericalOptionsPanel.getOptions())) { + return true; + } + if (!Comparing.equal(settingsService.getTableOptions(), tableOptionsPanel.getOptions())) { return true; } @@ -80,6 +91,7 @@ public void apply() { settingsService.setConsoleOptions(consoleOptionsPanel.getOptions()); settingsService.setExecutionOptions(executionOptionsPanel.getOptions()); settingsService.setInspectorOptions(inspectorOptionsPanel.getOptions()); + settingsService.setNumericalOptions(numericalOptionsPanel.getOptions()); } @Override @@ -88,5 +100,6 @@ public void reset() { consoleOptionsPanel.setOptions(settingsService.getConsoleOptions()); executionOptionsPanel.setOptions(settingsService.getExecutionOptions()); inspectorOptionsPanel.setOptions(settingsService.getInspectorOptions()); + numericalOptionsPanel.setOptions(settingsService.getNumericalOptions()); } } diff --git a/src/main/java/org/kdb/inside/brains/settings/KdbSettingsService.java b/src/main/java/org/kdb/inside/brains/settings/KdbSettingsService.java index 75847b9..71288f1 100644 --- a/src/main/java/org/kdb/inside/brains/settings/KdbSettingsService.java +++ b/src/main/java/org/kdb/inside/brains/settings/KdbSettingsService.java @@ -6,11 +6,13 @@ import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; +import com.intellij.util.xmlb.annotations.Transient; import org.jetbrains.annotations.Nullable; import org.kdb.inside.brains.core.ExecutionOptions; import org.kdb.inside.brains.core.InstanceOptions; import org.kdb.inside.brains.view.chart.ChartOptions; import org.kdb.inside.brains.view.console.ConsoleOptions; +import org.kdb.inside.brains.view.console.NumericalOptions; import org.kdb.inside.brains.view.console.TableOptions; import org.kdb.inside.brains.view.inspector.InspectorOptions; @@ -19,7 +21,7 @@ import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; -@State(name = "KdbSettings", storages = {@Storage("kdb-settings.xml")}) +@State(name = "KdbSettings", storages = {@Storage(value = "kdb-settings.xml")}) public class KdbSettingsService implements PersistentStateComponent { private final State myState = new State(); @@ -124,6 +126,17 @@ public void setChartOptions(ChartOptions options) { } } + public NumericalOptions getNumericalOptions() { + return myState.numericalOptions; + } + + public void setNumericalOptions(NumericalOptions options) { + if (!myState.numericalOptions.equals(options)) { + myState.numericalOptions.copyFrom(options); + notifySettingsChanged(myState.numericalOptions); + } + } + private void notifySettingsChanged(SettingsBean bean) { listeners.forEach(l -> l.settingsChanged(this, bean)); } @@ -143,6 +156,12 @@ public void loadState(State state) { myState.setInstanceOptions(state.instanceOptions); myState.setCredentialPlugins(state.credentialPlugins); myState.setInspectorOptions(state.inspectorOptions); + myState.setNumericalOptions(state.numericalOptions); + + // Legacy migration. To be removed one day + if (state.legacyConsoleOptions.floatPrecision != -1) { + myState.numericalOptions.setFloatPrecision(state.legacyConsoleOptions.floatPrecision); + } } public static KdbSettingsService getInstance() { @@ -152,7 +171,11 @@ public static KdbSettingsService getInstance() { return instance; } - static class State { + private static final class LegacyConsoleOptions { + private int floatPrecision; + } + + public static class State { private final List credentialPlugins = new ArrayList<>(); private final ChartOptions chartOptions = new ChartOptions(); private final TableOptions tableOptions = new TableOptions(); @@ -160,6 +183,11 @@ static class State { private final InstanceOptions instanceOptions = new InstanceOptions(); private final ExecutionOptions executionOptions = new ExecutionOptions(); private final InspectorOptions inspectorOptions = new InspectorOptions(); + private final NumericalOptions numericalOptions = new NumericalOptions(); + + @Transient + // Collection of all legacy options for migration copied from other options + private final LegacyConsoleOptions legacyConsoleOptions = new LegacyConsoleOptions(); public List getCredentialPlugins() { return credentialPlugins; @@ -186,6 +214,7 @@ public ConsoleOptions getConsoleOptions() { public void setConsoleOptions(ConsoleOptions consoleOptions) { this.consoleOptions.copyFrom(consoleOptions); + legacyConsoleOptions.floatPrecision = consoleOptions.getLegacyFloatPrecision(); } public InstanceOptions getInstanceOptions() { @@ -219,5 +248,13 @@ public ChartOptions getChartOptions() { public void setChartOptions(ChartOptions options) { chartOptions.copyFrom(options); } + + public NumericalOptions getNumericalOptions() { + return numericalOptions; + } + + public void setNumericalOptions(NumericalOptions options) { + numericalOptions.copyFrom(options); + } } } diff --git a/src/main/java/org/kdb/inside/brains/view/FormatterOptions.java b/src/main/java/org/kdb/inside/brains/view/FormatterOptions.java index 40a4a4b..79b91af 100644 --- a/src/main/java/org/kdb/inside/brains/view/FormatterOptions.java +++ b/src/main/java/org/kdb/inside/brains/view/FormatterOptions.java @@ -1,15 +1,71 @@ package org.kdb.inside.brains.view; -public interface FormatterOptions { - int getFloatPrecision(); +import org.kdb.inside.brains.settings.KdbSettingsService; +import org.kdb.inside.brains.view.console.ConsoleOptions; +import org.kdb.inside.brains.view.console.NumericalOptions; - boolean isWrapStrings(); +import java.math.RoundingMode; +import java.util.function.BooleanSupplier; - boolean isPrefixSymbols(); +public class FormatterOptions { + private final ConsoleOptions consoleOptions; + private final NumericalOptions numericalOptions; - boolean isEnlistArrays(); + private final BooleanSupplier thousandsSupplier; + private final BooleanSupplier scientificSupplier; - default boolean isThousandsSeparator() { - return false; + public FormatterOptions() { + this(KdbSettingsService.getInstance().getConsoleOptions(), KdbSettingsService.getInstance().getNumericalOptions()); } -} + + public FormatterOptions(ConsoleOptions consoleOptions, NumericalOptions numericalOptions) { + this(consoleOptions, numericalOptions, () -> false, numericalOptions::isScientificNotation); + } + + private FormatterOptions(ConsoleOptions consoleOptions, NumericalOptions numericalOptions, BooleanSupplier thousandsSupplier, BooleanSupplier scientificSupplier) { + this.consoleOptions = consoleOptions; + this.numericalOptions = numericalOptions; + this.thousandsSupplier = thousandsSupplier; + this.scientificSupplier = scientificSupplier; + } + + public boolean isWrapStrings() { + return consoleOptions.isWrapStrings(); + } + + public boolean isPrefixSymbols() { + return consoleOptions.isPrefixSymbols(); + } + + public boolean isEnlistArrays() { + return consoleOptions.isEnlistArrays(); + } + + public int getFloatPrecision() { + return numericalOptions.getFloatPrecision(); + } + + public RoundingMode getRoundingMode() { + return numericalOptions.getRoundingMode(); + } + + public boolean isScientificNotation() { + return scientificSupplier.getAsBoolean(); + } + + public boolean isThousandsSeparator() { + return thousandsSupplier.getAsBoolean(); + } + + public FormatterOptions withThousandsSeparator(BooleanSupplier thousands) { + return new FormatterOptions(consoleOptions, numericalOptions, thousands, scientificSupplier); + } + + public FormatterOptions withScientificNotation(BooleanSupplier scientific) { + return new FormatterOptions(consoleOptions, numericalOptions, thousandsSupplier, scientific); + } + + public FormatterOptions withThousandAndScientific(BooleanSupplier thousands, BooleanSupplier scientific) { + return new FormatterOptions(consoleOptions, numericalOptions, thousands, scientific); + } +} \ No newline at end of file diff --git a/src/main/java/org/kdb/inside/brains/view/KdbOutputFormatter.java b/src/main/java/org/kdb/inside/brains/view/KdbOutputFormatter.java index 2d4b95b..a5ca3d8 100644 --- a/src/main/java/org/kdb/inside/brains/view/KdbOutputFormatter.java +++ b/src/main/java/org/kdb/inside/brains/view/KdbOutputFormatter.java @@ -5,16 +5,14 @@ import org.jetbrains.annotations.NotNull; import org.kdb.inside.brains.KdbType; import org.kdb.inside.brains.core.KdbResult; -import org.kdb.inside.brains.settings.KdbSettingsService; -import org.kdb.inside.brains.view.console.ConsoleOptions; +import org.kdb.inside.brains.view.console.NumericalOptions; import java.lang.reflect.Array; +import java.math.RoundingMode; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; -import java.text.DateFormat; -import java.text.DecimalFormat; -import java.text.SimpleDateFormat; +import java.text.*; import java.util.UUID; import java.util.function.Function; @@ -26,11 +24,13 @@ public final class KdbOutputFormatter { private static final DateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy.MM.dd'T'HH:mm:ss.SSS"); private static final DateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy.MM.dd'D'HH:mm:ss"); - private static final DecimalFormat NUMBER_SEPARATOR = new DecimalFormat("#,##0"); - private static final DecimalFormat[] DECIMAL_FORMAT_SEPARATOR = new DecimalFormat[ConsoleOptions.MAX_DECIMAL_PRECISION + 1]; + private static final DecimalFormat INTEGER = new DecimalFormat("0"); + private static final DecimalFormat INTEGER_SEPARATOR = new DecimalFormat("#,##0"); + + private static final DecimalFormat[][] DECIMAL = new DecimalFormat[RoundingMode.values().length][NumericalOptions.MAX_DECIMAL_PRECISION + 1]; + private static final DecimalFormat[][] DECIMAL_SEPARATOR = new DecimalFormat[RoundingMode.values().length][NumericalOptions.MAX_DECIMAL_PRECISION + 1]; + private static final DecimalFormat[][] DECIMAL_SCIENTIFIC = new DecimalFormat[RoundingMode.values().length][NumericalOptions.MAX_DECIMAL_PRECISION + 1]; - private static final DecimalFormat NUMBER = new DecimalFormat("0"); - private static final DecimalFormat[] DECIMAL_FORMAT = new DecimalFormat[ConsoleOptions.MAX_DECIMAL_PRECISION + 1]; private static KdbOutputFormatter defaultInstance; static { @@ -39,11 +39,18 @@ public final class KdbOutputFormatter { DATETIME_FORMAT.setTimeZone(KxConnection.UTC_TIMEZONE); TIMESTAMP_FORMAT.setTimeZone(KxConnection.UTC_TIMEZONE); - DECIMAL_FORMAT[0] = new DecimalFormat("0."); - DECIMAL_FORMAT_SEPARATOR[0] = new DecimalFormat("#,##0."); - for (int i = 1; i <= ConsoleOptions.MAX_DECIMAL_PRECISION; i++) { - DECIMAL_FORMAT[i] = new DecimalFormat("0." + "#".repeat(i)); - DECIMAL_FORMAT_SEPARATOR[i] = new DecimalFormat("#,##0." + "#".repeat(i)); + for (RoundingMode mode : RoundingMode.values()) { + final int modeId = mode.ordinal(); + for (int precision = 0; precision < NumericalOptions.MAX_DECIMAL_PRECISION; precision++) { + DECIMAL[modeId][precision] = new DecimalFormat("0." + "#".repeat(precision)); + DECIMAL[modeId][precision].setRoundingMode(mode); + + DECIMAL_SEPARATOR[modeId][precision] = new DecimalFormat("#,##0." + "#".repeat(precision)); + DECIMAL_SEPARATOR[modeId][precision].setRoundingMode(mode); + + DECIMAL_SCIENTIFIC[modeId][precision] = createExponentialFormat("0." + "#".repeat(precision)); + DECIMAL_SCIENTIFIC[modeId][precision].setRoundingMode(mode); + } } } @@ -77,7 +84,7 @@ public String resultToString(KdbResult result, boolean prefixSymbol, boolean wra */ public static KdbOutputFormatter getDefault() { if (defaultInstance == null) { - defaultInstance = new KdbOutputFormatter(KdbSettingsService.getInstance().getConsoleOptions()); + defaultInstance = new KdbOutputFormatter(new FormatterOptions()); } return defaultInstance; } @@ -471,12 +478,23 @@ public String formatFloats(float[] v) { return b.toString(); } - @NotNull - public String formatDouble(double v) { - if (v % 1 == 0) { - return ((long) v) + "f"; - } - return isNull(v) ? "0n" : doubleToStr(v); + private static DecimalFormat createExponentialFormat(String pattern) { + final DecimalFormat decimalFormat = new DecimalFormat(pattern + "E000") { + @Override + public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) { + final StringBuffer format = super.format(number, result, fieldPosition); + if (number < -1 || number > 1) { + format.insert(format.length() - 3, "+"); + } + return format; + } + }; + + final DecimalFormatSymbols symbols = decimalFormat.getDecimalFormatSymbols(); + symbols.setExponentSeparator("e"); + decimalFormat.setDecimalFormatSymbols(symbols); + + return decimalFormat; } @NotNull @@ -649,16 +667,33 @@ private String intToStr(int s) { return options.isThousandsSeparator() ? longToStr(s) : String.valueOf(s); } - private String longToStr(long s) { - return (options.isThousandsSeparator() ? NUMBER_SEPARATOR : NUMBER).format(s); + @NotNull + public String formatDouble(double v) { + if (v % 1 == 0) { + if (isScientificNotation(v)) { + return doubleToStr(v); + } + return ((long) v) + "f"; + } + return isNull(v) ? "0n" : doubleToStr(v); } private String floatToStr(float s) { return doubleToStr(s); } + private String longToStr(long s) { + return (options.isThousandsSeparator() ? INTEGER_SEPARATOR : INTEGER).format(s); + } + private String doubleToStr(double s) { - return (options.isThousandsSeparator() ? DECIMAL_FORMAT_SEPARATOR : DECIMAL_FORMAT)[options.getFloatPrecision()].format(s); + final DecimalFormat[][] formats; + if (isScientificNotation(s)) { + formats = DECIMAL_SCIENTIFIC; + } else { + formats = options.isThousandsSeparator() ? DECIMAL_SEPARATOR : DECIMAL; + } + return formats[options.getRoundingMode().ordinal()][options.getFloatPrecision()].format(s); } private boolean isNull(short v) { @@ -689,4 +724,8 @@ private String getKdbTypeName(Class aClass) { final KdbType kdbType = KdbType.typeOf(aClass); return kdbType == null || kdbType == KdbType.ANY ? null : kdbType.getTypeName(); } -} + + private boolean isScientificNotation(double s) { + return options.isScientificNotation() && !Double.isInfinite(s) && !Double.isNaN(s) && ((s > 0 && s <= 0.00001) || (s >= -0.00001 && s < 0) || s <= -10000000 || s >= 10000000); + } +} \ No newline at end of file diff --git a/src/main/java/org/kdb/inside/brains/view/console/ConsoleOptions.java b/src/main/java/org/kdb/inside/brains/view/console/ConsoleOptions.java index e3260ca..2b57c09 100644 --- a/src/main/java/org/kdb/inside/brains/view/console/ConsoleOptions.java +++ b/src/main/java/org/kdb/inside/brains/view/console/ConsoleOptions.java @@ -1,12 +1,11 @@ package org.kdb.inside.brains.view.console; +import com.intellij.util.xmlb.annotations.Property; import org.kdb.inside.brains.settings.SettingsBean; -import org.kdb.inside.brains.view.FormatterOptions; import java.util.Objects; -public final class ConsoleOptions implements SettingsBean, FormatterOptions { - private int floatPrecision = 7; +public final class ConsoleOptions implements SettingsBean { private boolean wrapStrings = true; private boolean prefixSymbols = true; private boolean enlistArrays = true; @@ -16,12 +15,16 @@ public final class ConsoleOptions implements SettingsBean, Forma private boolean clearTableResult = true; private ConsoleSplitType splitType = ConsoleSplitType.NO; - public static final int MAX_DECIMAL_PRECISION = 16; + @Property + @Deprecated(forRemoval = true) + private int floatPrecision = -1; - public ConsoleOptions() { + public int getLegacyFloatPrecision() { + int asd = floatPrecision; + floatPrecision = -1; + return asd; } - @Override public boolean isWrapStrings() { return wrapStrings; } @@ -30,7 +33,6 @@ public void setWrapStrings(boolean wrapStrings) { this.wrapStrings = wrapStrings; } - @Override public boolean isPrefixSymbols() { return prefixSymbols; } @@ -47,16 +49,6 @@ public void setDictAsTable(boolean dictAsTable) { this.dictAsTable = dictAsTable; } - @Override - public int getFloatPrecision() { - return floatPrecision; - } - - public void setFloatPrecision(int floatPrecision) { - this.floatPrecision = floatPrecision; - } - - @Override public boolean isEnlistArrays() { return enlistArrays; } @@ -114,7 +106,6 @@ public void copyFrom(ConsoleOptions options) { this.wrapStrings = options.wrapStrings; this.prefixSymbols = options.prefixSymbols; this.enlistArrays = options.enlistArrays; - this.floatPrecision = options.floatPrecision; this.listAsTable = options.listAsTable; this.dictAsTable = options.dictAsTable; this.splitType = options.splitType; @@ -125,8 +116,7 @@ public void copyFrom(ConsoleOptions options) { @Override public String toString() { return "ConsoleOptions{" + - "floatPrecision=" + floatPrecision + - ", wrapStrings=" + wrapStrings + + "wrapStrings=" + wrapStrings + ", prefixSymbols=" + prefixSymbols + ", enlistArrays=" + enlistArrays + ", dictAsTable=" + dictAsTable + diff --git a/src/main/java/org/kdb/inside/brains/view/console/ConsoleOptionsPanel.java b/src/main/java/org/kdb/inside/brains/view/console/ConsoleOptionsPanel.java index 76c8f37..6d6f148 100644 --- a/src/main/java/org/kdb/inside/brains/view/console/ConsoleOptionsPanel.java +++ b/src/main/java/org/kdb/inside/brains/view/console/ConsoleOptionsPanel.java @@ -1,7 +1,6 @@ package org.kdb.inside.brains.view.console; import com.intellij.openapi.ui.ComboBox; -import com.intellij.ui.JBIntSpinner; import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBLabel; import com.intellij.util.ui.FormBuilder; @@ -17,7 +16,6 @@ public class ConsoleOptionsPanel extends JPanel { private final JBCheckBox dictAsTable = new JBCheckBox("Show dict as table"); private final JBCheckBox consoleBackground = new JBCheckBox("Use an instance color for console background"); private final JBCheckBox clearTableResult = new JBCheckBox("Clear 'Table Result' if result is not a table"); - private final JBIntSpinner floatPrecisionEditor = new JBIntSpinner(7, 0, ConsoleOptions.MAX_DECIMAL_PRECISION); private final ComboBox splitTypes = new ComboBox<>(ConsoleSplitType.values()); public ConsoleOptionsPanel() { @@ -33,7 +31,7 @@ public ConsoleOptionsPanel() { formBuilder.addComponent(prefixSymbols); formBuilder.addComponent(clearTableResult); formBuilder.addComponent(consoleBackground); - formBuilder.addLabeledComponent("Float precision: ", floatPrecisionEditor); + createSplitTypes(formBuilder); add(formBuilder.getPanel()); @@ -59,7 +57,6 @@ public Component getListCellRendererComponent(JList list, Object value, int i public ConsoleOptions getOptions() { final ConsoleOptions consoleOptions = new ConsoleOptions(); consoleOptions.setEnlistArrays(enlistArrays.isSelected()); - consoleOptions.setFloatPrecision(floatPrecisionEditor.getNumber()); consoleOptions.setWrapStrings(wrapString.isSelected()); consoleOptions.setPrefixSymbols(prefixSymbols.isSelected()); consoleOptions.setListAsTable(listAsTable.isSelected()); @@ -71,7 +68,6 @@ public ConsoleOptions getOptions() { } public void setOptions(ConsoleOptions consoleOptions) { - floatPrecisionEditor.setNumber(consoleOptions.getFloatPrecision()); enlistArrays.setSelected(consoleOptions.isEnlistArrays()); wrapString.setSelected(consoleOptions.isWrapStrings()); prefixSymbols.setSelected(consoleOptions.isPrefixSymbols()); diff --git a/src/main/java/org/kdb/inside/brains/view/console/NumericalOptions.java b/src/main/java/org/kdb/inside/brains/view/console/NumericalOptions.java new file mode 100644 index 0000000..4bf404b --- /dev/null +++ b/src/main/java/org/kdb/inside/brains/view/console/NumericalOptions.java @@ -0,0 +1,65 @@ +package org.kdb.inside.brains.view.console; + +import org.kdb.inside.brains.settings.SettingsBean; + +import java.math.RoundingMode; +import java.util.Objects; + +public class NumericalOptions implements SettingsBean { + public static final int MAX_DECIMAL_PRECISION = 16; + private int floatPrecision = 7; + private boolean scientificNotation = false; + private RoundingMode roundingMode = RoundingMode.HALF_EVEN; + + public int getFloatPrecision() { + return floatPrecision; + } + + public void setFloatPrecision(int floatPrecision) { + this.floatPrecision = floatPrecision; + } + + public boolean isScientificNotation() { + return scientificNotation; + } + + public void setScientificNotation(boolean scientificNotation) { + this.scientificNotation = scientificNotation; + } + + public RoundingMode getRoundingMode() { + return roundingMode; + } + + public void setRoundingMode(RoundingMode roundingMode) { + this.roundingMode = roundingMode; + } + + @Override + public void copyFrom(NumericalOptions o) { + floatPrecision = o.floatPrecision; + scientificNotation = o.scientificNotation; + roundingMode = o.roundingMode; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof NumericalOptions that)) return false; + return floatPrecision == that.floatPrecision && scientificNotation == that.scientificNotation && roundingMode == that.roundingMode; + } + + @Override + public int hashCode() { + return Objects.hash(floatPrecision, scientificNotation, roundingMode); + } + + @Override + public String toString() { + return "NumericalOptions{" + + "floatPrecision=" + floatPrecision + + ", scientificNotation=" + scientificNotation + + ", roundingMode=" + roundingMode + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/org/kdb/inside/brains/view/console/NumericalOptionsPanel.java b/src/main/java/org/kdb/inside/brains/view/console/NumericalOptionsPanel.java new file mode 100644 index 0000000..b674d14 --- /dev/null +++ b/src/main/java/org/kdb/inside/brains/view/console/NumericalOptionsPanel.java @@ -0,0 +1,102 @@ +package org.kdb.inside.brains.view.console; + +import com.intellij.openapi.ui.ComboBox; +import com.intellij.ui.ContextHelpLabel; +import com.intellij.ui.JBIntSpinner; +import com.intellij.ui.components.JBCheckBox; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.ui.FormBuilder; + +import javax.swing.*; +import java.awt.*; +import java.math.RoundingMode; + +public class NumericalOptionsPanel extends JPanel { + private final ComboBox roungingComboBox = new ComboBox<>(); + private final JBCheckBox scientificNotation = new JBCheckBox("Show decimals in scientific notation (1.23e+014)"); + private final JBIntSpinner floatPrecisionEditor = new JBIntSpinner(7, 0, NumericalOptions.MAX_DECIMAL_PRECISION); + + public NumericalOptionsPanel() { + super(new BorderLayout()); + + roungingComboBox.addItem(new RoundingItem(RoundingMode.UP, "Up", "Rounding mode to round away from zero.")); + roungingComboBox.addItem(new RoundingItem(RoundingMode.DOWN, "Down", "Rounding mode to round towards zero.")); + roungingComboBox.addItem(new RoundingItem(RoundingMode.CEILING, "Ceiling", "Rounding mode to round towards positive infinity.")); + roungingComboBox.addItem(new RoundingItem(RoundingMode.FLOOR, "Floor", "Rounding mode to round towards negative infinity.")); + roungingComboBox.addItem(new RoundingItem(RoundingMode.HALF_UP, "Half Up", "Rounding mode to round towards \"nearest neighbor\" unless both neighbors are equidistant, in which case round up.")); + roungingComboBox.addItem(new RoundingItem(RoundingMode.HALF_DOWN, "Half Down", "Rounding mode to round towards \"nearest neighbor\" unless both neighbors are equidistant, in which case round down.")); + roungingComboBox.addItem(new RoundingItem(RoundingMode.HALF_EVEN, "Half Even", "Rounding mode to round towards the \"nearest neighbor\" unless both neighbors are equidistant, in which case, round towards the even neighbor.")); + roungingComboBox.setRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + return super.getListCellRendererComponent(list, ((RoundingItem) value).name, index, isSelected, cellHasFocus); + } + }); + + final var formBuilder = FormBuilder.createFormBuilder(); + addPrecision(formBuilder); + addRoundingMode(formBuilder); + addScientificNotation(formBuilder); + + add(formBuilder.getPanel()); + } + + public NumericalOptions getOptions() { + final NumericalOptions options = new NumericalOptions(); + options.setRoundingMode(roungingComboBox.getItem().mode()); + options.setFloatPrecision(floatPrecisionEditor.getNumber()); + options.setScientificNotation(scientificNotation.isSelected()); + return options; + } + + public void setOptions(NumericalOptions options) { + roungingComboBox.setSelectedIndex(findRoundingItem(options.getRoundingMode())); + floatPrecisionEditor.setNumber(options.getFloatPrecision()); + scientificNotation.setSelected(options.isScientificNotation()); + } + + private int findRoundingItem(RoundingMode mode) { + final int itemCount = roungingComboBox.getItemCount(); + for (int i = 0; i < itemCount; i++) { + if (mode == roungingComboBox.getItemAt(i).mode) { + return i; + } + } + return -1; + } + + private void addPrecision(FormBuilder formBuilder) { + formBuilder.addLabeledComponent("Float precision: ", floatPrecisionEditor); + } + + private void addRoundingMode(FormBuilder formBuilder) { + final StringBuilder b = new StringBuilder(""); + final int itemCount = roungingComboBox.getItemCount(); + for (int i = 0; i < itemCount; i++) { + final RoundingItem itemAt = roungingComboBox.getItemAt(i); + b.append("").append(itemAt.name()).append(": ").append("").append(itemAt.desc()); + b.append("

"); + } + b.append(""); + + JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + p.add(new JBLabel("Rounding mode: ")); + p.add(roungingComboBox); + p.add(Box.createHorizontalStrut(5)); + p.add(ContextHelpLabel.create(b.toString())); + formBuilder.addComponent(p); + } + + private void addScientificNotation(FormBuilder formBuilder) { + final ContextHelpLabel infoLabel = ContextHelpLabel.create("Display decimal numbers less or equal 10-5 or more or equal 107 in scientific notation (like 1.2345e-003)"); + + JPanel p = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + p.add(scientificNotation); + p.add(Box.createHorizontalStrut(5)); + p.add(infoLabel); + formBuilder.addComponent(p); + } + + record RoundingItem(RoundingMode mode, String name, String desc) { + } +} \ No newline at end of file diff --git a/src/main/java/org/kdb/inside/brains/view/console/TableOptions.java b/src/main/java/org/kdb/inside/brains/view/console/TableOptions.java index 9281ddf..fb3c2e9 100644 --- a/src/main/java/org/kdb/inside/brains/view/console/TableOptions.java +++ b/src/main/java/org/kdb/inside/brains/view/console/TableOptions.java @@ -14,7 +14,6 @@ public class TableOptions implements SettingsBean { private boolean expandTable = true; private boolean thousandsSeparator = false; - public boolean isStriped() { return striped; } diff --git a/src/main/java/org/kdb/inside/brains/view/console/table/TableResultView.java b/src/main/java/org/kdb/inside/brains/view/console/table/TableResultView.java index 7bea8e0..b999d9f 100644 --- a/src/main/java/org/kdb/inside/brains/view/console/table/TableResultView.java +++ b/src/main/java/org/kdb/inside/brains/view/console/table/TableResultView.java @@ -29,7 +29,7 @@ import org.kdb.inside.brains.view.FormatterOptions; import org.kdb.inside.brains.view.KdbOutputFormatter; import org.kdb.inside.brains.view.chart.ChartActionGroup; -import org.kdb.inside.brains.view.console.ConsoleOptions; +import org.kdb.inside.brains.view.console.NumericalOptions; import org.kdb.inside.brains.view.console.TableOptions; import org.kdb.inside.brains.view.export.ClipboardExportAction; import org.kdb.inside.brains.view.export.ExportDataProvider; @@ -57,7 +57,8 @@ public class TableResultView extends NonOpaquePanel implements DataProvider, Exp private final ToggleAction searchAction; private final ToggleAction filterAction; private final ToggleAction showIndexAction; - private final ToggleAction showThousandsAction; + private final ShowTableOptionAction showThousandsAction; + private final ShowTableOptionAction showScientificAction; private final ActionGroup chartActionGroup; private final ActionGroup exportActionGroup; @@ -75,8 +76,6 @@ public class TableResultView extends NonOpaquePanel implements DataProvider, Exp private ColumnsFilterPanel columnsFilter; private TableResultStatusPanel statusBar; - private BooleanSupplier thousandsSeparator; - private static final Color DECORATED_ROW_COLOR = UIUtil.getDecoratedRowColor(); private static final JBColor SEARCH_FOREGROUND = new JBColor(Gray._50, Gray._0); private static final JBColor SEARCH_BACKGROUND = UIUtil.getSearchMatchGradientStartColor(); @@ -94,8 +93,9 @@ public TableResultView(Project project, TableMode mode) { public TableResultView(Project project, TableMode mode, BiConsumer repeater) { this.project = project; - this.tableOptions = KdbSettingsService.getInstance().getTableOptions(); - this.thousandsSeparator = tableOptions::isThousandsSeparator; + final KdbSettingsService settingsService = KdbSettingsService.getInstance(); + + this.tableOptions = settingsService.getTableOptions(); this.formatter = createOutputFormatter(); myTable = createTable(); @@ -128,7 +128,8 @@ public void mouseReleased(MouseEvent e) { searchAction = createSearchAction(); filterAction = createFilterAction(); showIndexAction = createShowIndexAction(); - showThousandsAction = createShowThousandsAction(); + showScientificAction = createShowScientificAction(settingsService.getNumericalOptions()); + showThousandsAction = createShowThousandsAction(settingsService.getTableOptions()); chartActionGroup = new ChartActionGroup(myTable); exportActionGroup = ExportDataProvider.createActionGroup(project, this); @@ -190,22 +191,13 @@ public void update(@NotNull AnActionEvent e) { } @NotNull - private ToggleAction createShowThousandsAction() { - final ToggleAction action = new EdtToggleAction("Show Thousands Separator", "Format numbers with thousands separator", KdbIcons.Console.TableThousands) { - @Override - public boolean isSelected(@NotNull AnActionEvent e) { - return thousandsSeparator.getAsBoolean(); - } + private ShowTableOptionAction createShowScientificAction(NumericalOptions options) { + return new ShowTableOptionAction("Show Scientific Notation", "Format as computerized scientific notation", KdbIcons.Console.TableScientific, KeyEvent.VK_E, options::isScientificNotation); + } - @Override - public void setSelected(@NotNull AnActionEvent e, boolean state) { - thousandsSeparator = () -> state; - myTable.repaint(); - statusBar.recalculateValues(); - } - }; - action.registerCustomShortcutSet(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK, myTable); - return action; + @NotNull + private ShowTableOptionAction createShowThousandsAction(TableOptions options) { + return new ShowTableOptionAction("Show Thousands Separator", "Format numbers with thousands separator", KdbIcons.Console.TableThousands, KeyEvent.VK_S, options::isThousandsSeparator); } @NotNull @@ -273,34 +265,8 @@ public void setSelected(@NotNull AnActionEvent e, boolean state) { @NotNull private KdbOutputFormatter createOutputFormatter() { - return new KdbOutputFormatter(new FormatterOptions() { - final ConsoleOptions consoleOptions = KdbSettingsService.getInstance().getConsoleOptions(); - - @Override - public int getFloatPrecision() { - return consoleOptions.getFloatPrecision(); - } - - @Override - public boolean isWrapStrings() { - return consoleOptions.isWrapStrings(); - } - - @Override - public boolean isPrefixSymbols() { - return consoleOptions.isPrefixSymbols(); - } - - @Override - public boolean isEnlistArrays() { - return consoleOptions.isEnlistArrays(); - } - - @Override - public boolean isThousandsSeparator() { - return thousandsSeparator.getAsBoolean(); - } - }); + // We can't pass the supplier itself here as it's changed inside the action so use a wrapper + return new KdbOutputFormatter(new FormatterOptions().withThousandAndScientific(() -> showThousandsAction.supplier.getAsBoolean(), () -> showScientificAction.supplier.getAsBoolean())); } @NotNull @@ -456,6 +422,7 @@ private JComponent createLeftToolbar() { view.addSeparator(); view.add(showIndexAction); view.add(showThousandsAction); + view.add(showScientificAction); group.add(view); group.addSeparator(); @@ -487,8 +454,11 @@ private ActionGroup createPopupMenu() { } group.add(searchAction); group.addSeparator(); - group.add(showIndexAction); - group.add(showThousandsAction); + final DefaultActionGroup view = new PopupActionGroup("View Settings", AllIcons.Actions.Show); + view.add(showIndexAction); + view.add(showThousandsAction); + view.add(showScientificAction); + group.add(view); group.addSeparator(); group.addAll(exportActionGroup); group.addSeparator(); @@ -697,4 +667,30 @@ private void modelBeenUpdated(FindModel findModel) { record ExpandedTabDetails(String name, String description) { } + + private class ShowTableOptionAction extends EdtToggleAction { + private BooleanSupplier supplier; + + public ShowTableOptionAction(String text, String description, Icon icon, int keyCode, BooleanSupplier supplier) { + super(text, description, icon); + this.supplier = supplier; + registerCustomShortcutSet(keyCode, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK, myTable); + } + + public boolean isSelected() { + return supplier.getAsBoolean(); + } + + @Override + public boolean isSelected(@NotNull AnActionEvent e) { + return isSelected(); + } + + @Override + public void setSelected(@NotNull AnActionEvent e, boolean state) { + supplier = () -> state; + myTable.repaint(); + statusBar.recalculateValues(); + } + } } \ No newline at end of file diff --git a/src/main/resources/org/kdb/inside/brains/icons/console/scientificNotation.svg b/src/main/resources/org/kdb/inside/brains/icons/console/scientificNotation.svg new file mode 100644 index 0000000..a67e1ca --- /dev/null +++ b/src/main/resources/org/kdb/inside/brains/icons/console/scientificNotation.svg @@ -0,0 +1,4 @@ + + + E + diff --git a/src/main/resources/org/kdb/inside/brains/icons/console/scientificNotation_dark.svg b/src/main/resources/org/kdb/inside/brains/icons/console/scientificNotation_dark.svg new file mode 100644 index 0000000..8fb1719 --- /dev/null +++ b/src/main/resources/org/kdb/inside/brains/icons/console/scientificNotation_dark.svg @@ -0,0 +1,4 @@ + + + E + \ No newline at end of file diff --git a/src/test/java/org/kdb/inside/brains/view/console/KdbOutputFormatterTest.java b/src/test/java/org/kdb/inside/brains/view/console/KdbOutputFormatterTest.java index ba39409..2e0de2d 100644 --- a/src/test/java/org/kdb/inside/brains/view/console/KdbOutputFormatterTest.java +++ b/src/test/java/org/kdb/inside/brains/view/console/KdbOutputFormatterTest.java @@ -3,8 +3,10 @@ import kx.c; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.kdb.inside.brains.view.FormatterOptions; import org.kdb.inside.brains.view.KdbOutputFormatter; +import java.math.RoundingMode; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; @@ -13,12 +15,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals; class KdbOutputFormatterTest { - private ConsoleOptions options; + private FormatterOptions options; + + private ConsoleOptions consoleOptions; + private NumericalOptions numericalOptions; @BeforeEach void init() { - options = new ConsoleOptions(); - options.setEnlistArrays(false); + consoleOptions = new ConsoleOptions(); + consoleOptions.setEnlistArrays(false); + + numericalOptions = new NumericalOptions(); + + options = new FormatterOptions(consoleOptions, numericalOptions); } @Test @@ -299,16 +308,91 @@ void nulls() { @Test void precision() { - options.setFloatPrecision(15); + numericalOptions.setScientificNotation(false); + numericalOptions.setFloatPrecision(15); assertEquals("24.123456789098764", convert(24.1234567890987654321)); - options.setFloatPrecision(2); + numericalOptions.setFloatPrecision(2); assertEquals("24.12", convert(24.1234567890987654321)); - options.setFloatPrecision(0); + numericalOptions.setFloatPrecision(0); assertEquals("24.", convert(24.1234567890987654321)); } + @Test + void scientists() { + numericalOptions.setScientificNotation(true); + assertEquals("0n", convert(Double.NaN)); + assertEquals("-\u221E", convert(Double.NEGATIVE_INFINITY)); + assertEquals("\u221E", convert(Double.POSITIVE_INFINITY)); + + assertEquals("0f", convert(0.)); + assertEquals("0.1", convert(0.1)); + assertEquals("0.0001", convert(0.0001)); + assertEquals("1e-005", convert(0.00001)); + assertEquals("1f", convert(1.)); + assertEquals("1000000f", convert(1000000.)); + assertEquals("1e+007", convert(10000000.)); + assertEquals("1e+008", convert(100000000.)); + assertEquals("1e+009", convert(1000000000.)); + assertEquals("-0.1", convert(-0.1)); + assertEquals("-0.0001", convert(-0.0001)); + assertEquals("-1e-005", convert(-0.00001)); + assertEquals("-1f", convert(-1.)); + assertEquals("-1000000f", convert(-1000000.)); + assertEquals("-1e+007", convert(-10000000.)); + assertEquals("-1e+008", convert(-100000000.)); + assertEquals("-1e+009", convert(-1000000000.)); + + numericalOptions.setFloatPrecision(15); + assertEquals("2.567575757567641e+014", convert(256757575756764.1234567890987654321)); + + numericalOptions.setFloatPrecision(2); + assertEquals("2.57e+014", convert(256757575756764.1234567890987654321)); + + numericalOptions.setFloatPrecision(0); + assertEquals("3.e+014", convert(256757575756764.1234567890987654321)); + } + + @Test + void thousands() { + assertEquals("1", convert(1L)); + assertEquals("1000", convert(1000L)); + assertEquals("1000000.123", convert(1000000.123)); + + options = options.withThousandsSeparator(() -> true); + assertEquals("1", convert(1L)); + assertEquals("1,000", convert(1000L)); + assertEquals("1,000,000.123", convert(1000000.123)); + } + + @Test + void rounding() { + numericalOptions.setFloatPrecision(0); + final double[] in = new double[]{5.5, 2.5, 1.6, 1.1, 1.0, -1.0, -1.1, -1.6, -2.5, -5.5}; + + numericalOptions.setRoundingMode(RoundingMode.UP); + assertEquals("6. 3. 2. 2. 1. -1. -2. -2. -3. -6.", convert(in)); + + numericalOptions.setRoundingMode(RoundingMode.DOWN); + assertEquals("5. 2. 1. 1. 1. -1. -1. -1. -2. -5.", convert(in)); + + numericalOptions.setRoundingMode(RoundingMode.CEILING); + assertEquals("6. 3. 2. 2. 1. -1. -1. -1. -2. -5.", convert(in)); + + numericalOptions.setRoundingMode(RoundingMode.FLOOR); + assertEquals("5. 2. 1. 1. 1. -1. -2. -2. -3. -6.", convert(in)); + + numericalOptions.setRoundingMode(RoundingMode.HALF_UP); + assertEquals("6. 3. 2. 1. 1. -1. -1. -2. -3. -6.", convert(in)); + + numericalOptions.setRoundingMode(RoundingMode.HALF_DOWN); + assertEquals("5. 2. 2. 1. 1. -1. -1. -2. -2. -5.", convert(in)); + + numericalOptions.setRoundingMode(RoundingMode.HALF_EVEN); + assertEquals("6. 2. 2. 1. 1. -1. -1. -2. -2. -6.", convert(in)); + } + private String convert(Object o) { return new KdbOutputFormatter(options).objectToString(o); } diff --git a/version.properties b/version.properties index 301cf2e..f35aee1 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -pluginVersion=5.3.0 \ No newline at end of file +pluginVersion=5.4.0 \ No newline at end of file