From a098965d4c07643d41cf80ee1ba1cf8f5101daf3 Mon Sep 17 00:00:00 2001 From: arlyon Date: Sun, 4 Mar 2018 23:21:14 +0000 Subject: [PATCH 1/2] extract out number formatting utility Add three types of formatter/converter: - runescape style (which matches the one in game) - "pretty" style with locale formatting and 3 sig figs - string to number for converting the other direction --- .../net/runelite/client/game/ItemManager.java | 21 -- .../runelite/client/util/StackFormatter.java | 183 ++++++++++++++++++ .../client/util/StackFormatterTest.java | 106 ++++++++++ 3 files changed, 289 insertions(+), 21 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/util/StackFormatter.java create mode 100644 runelite-client/src/test/java/net/runelite/client/util/StackFormatterTest.java diff --git a/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java b/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java index 4053356cb2..6fba1ddaa7 100644 --- a/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java +++ b/runelite-client/src/main/java/net/runelite/client/game/ItemManager.java @@ -185,27 +185,6 @@ public class ItemManager return itemCompositions.getUnchecked(itemId); } - /** - * Convert a quantity to stack size - * - * @param quantity - * @return - */ - public static String quantityToStackSize(int quantity) - { - if (quantity >= 10_000_000) - { - return quantity / 1_000_000 + "M"; - } - - if (quantity >= 100_000) - { - return quantity / 1_000 + "K"; - } - - return "" + quantity; - } - /** * Loads item sprite from game, makes transparent, and generates image * diff --git a/runelite-client/src/main/java/net/runelite/client/util/StackFormatter.java b/runelite-client/src/main/java/net/runelite/client/util/StackFormatter.java new file mode 100644 index 0000000000..f970187419 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/util/StackFormatter.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2018, arlyon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.util; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A set of utility functions to use when + * formatting numbers for to stack sizes. + */ +public class StackFormatter +{ + /** + * A list of suffixes to use when formatting stack sizes. + */ + private static final String[] SUFFIXES = {"", "K", "M", "B"}; + + /** + * A pattern to match a value suffix (K, M etc) in a string. + */ + private static final Pattern SUFFIX_PATTERN = Pattern.compile("^-?[0-9,.]+([a-zA-Z]?)$"); + + /** + * A number formatter + */ + private static final NumberFormat NUMBER_FORMATTER = NumberFormat.getInstance(); + + /** + * Convert a quantity to a nicely formatted stack size. + * See the StackFormatterTest to see expected output. + * + * @param quantity The quantity to convert. + * @return A condensed version, with commas, K, M or B + * as needed to 3 significant figures. + */ + public static String quantityToStackSize(int quantity) + { + if (quantity < 0) + { + // Integer.MIN_VALUE = -1 * Integer.MIN_VALUE so we need to correct for it. + return "-" + quantityToStackSize(quantity == Integer.MIN_VALUE ? Integer.MAX_VALUE : -quantity); + } + else if (quantity < 10_000) + { + return NUMBER_FORMATTER.format(quantity); + } + + String suffix = SUFFIXES[0]; + int divideBy = 1; + + // determine correct suffix by iterating backward through the list + // of suffixes until the suffix results in a value >= 1 + for (int i = (SUFFIXES.length - 1); i >= 0; i--) + { + divideBy = (int) Math.pow(10, i * 3); + if ((float) quantity / divideBy >= 1) + { + suffix = SUFFIXES[i]; + break; + } + } + + // get locale formatted string + String formattedString = NUMBER_FORMATTER.format((float) quantity / divideBy); + + // strip down any digits past the 4 first + formattedString = (formattedString.length() > 4 ? formattedString.substring(0, 4) : formattedString); + + // make sure the last character is not a "." + return (formattedString.endsWith(".") ? formattedString.substring(0, 3) : formattedString) + suffix; + } + + /** + * Convert a quantity to stack size as it would + * appear in RuneScape. + * + * @param quantity The quantity to convert. + * @return The stack size as it would appear in RS, + * with K after 100,000 and M after 10,000,000 + */ + public static String quantityToRSStackSize(int quantity) + { + if (quantity == Integer.MIN_VALUE) + { + // Integer.MIN_VALUE = Integer.MIN_VALUE * -1 so we need to correct for it. + return "-" + quantityToRSStackSize(Integer.MAX_VALUE); + } + else if (quantity < 0) + { + return "-" + quantityToRSStackSize(-quantity); + } + else if (quantity < 100_000) + { + return Integer.toString(quantity); + } + else if (quantity < 10_000_000) + { + return quantity / 1_000 + "K"; + } + else + { + return quantity / 1_000_000 + "M"; + } + } + + /** + * Converts a string representation of a stack + * back to (close to) it's original value. + * + * @param string The string to convert. + * @return A long representation of it. + */ + public static long stackSizeToQuantity(String string) throws ParseException + { + int multiplier = getMultiplier(string); + float parsedValue = NUMBER_FORMATTER.parse(string).floatValue(); + return (long) (parsedValue * multiplier); + } + + /** + * Calculates, given a string with a value denominator (ex. 20K) + * the multiplier that the denominator represents (in this case 1000). + * + * @param string The string to check. + * @return The value of the value denominator. + * @throws ParseException When the denominator does not match a known value. + */ + private static int getMultiplier(String string) throws ParseException + { + String suffix; + Matcher matcher = SUFFIX_PATTERN.matcher(string); + if (matcher.find()) + { + suffix = matcher.group(1); + } + else + { + throw new ParseException(string + " does not resemble a properly formatted stack.", string.length() - 1); + } + + if (!suffix.equals("")) + { + for (int i = 1; i < SUFFIXES.length; i++) + { + if (SUFFIXES[i].equals(suffix.toUpperCase())) + { + return (int) Math.pow(10, i * 3); + } + } + + throw new ParseException("Invalid Suffix: " + suffix, string.length() - 1); + } + else + { + return 1; + } + } +} diff --git a/runelite-client/src/test/java/net/runelite/client/util/StackFormatterTest.java b/runelite-client/src/test/java/net/runelite/client/util/StackFormatterTest.java new file mode 100644 index 0000000000..79be1cba7d --- /dev/null +++ b/runelite-client/src/test/java/net/runelite/client/util/StackFormatterTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2018, arlyon + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.util; + +import java.text.NumberFormat; +import java.text.ParseException; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import org.junit.Test; + +public class StackFormatterTest +{ + + @Test + public void quantityToRSStackSize() + { + assertEquals("0", StackFormatter.quantityToRSStackSize(0)); + assertEquals("99999", StackFormatter.quantityToRSStackSize(99_999)); + assertEquals("100K", StackFormatter.quantityToRSStackSize(100_000)); + assertEquals("10M", StackFormatter.quantityToRSStackSize(10_000_000)); + assertEquals("2147M", StackFormatter.quantityToRSStackSize(Integer.MAX_VALUE)); + + assertEquals("0", StackFormatter.quantityToRSStackSize(-0)); + assertEquals("-400", StackFormatter.quantityToRSStackSize(-400)); + assertEquals("-400K", StackFormatter.quantityToRSStackSize(-400_000)); + assertEquals("-40M", StackFormatter.quantityToRSStackSize(-40_000_000)); + assertEquals("-2147M", StackFormatter.quantityToRSStackSize(Integer.MIN_VALUE)); + } + + @Test + public void quantityToStackSize() + { + assertEquals("0", StackFormatter.quantityToStackSize(0)); + assertEquals("999", StackFormatter.quantityToStackSize(999)); + assertEquals(NumberFormat.getIntegerInstance().format(1000), StackFormatter.quantityToStackSize(1000)); + assertEquals(NumberFormat.getIntegerInstance().format(9450), StackFormatter.quantityToStackSize(9450)); + assertEquals(NumberFormat.getNumberInstance().format(14.5) + "K", StackFormatter.quantityToStackSize(14_500)); + assertEquals(NumberFormat.getNumberInstance().format(99.9) + "K", StackFormatter.quantityToStackSize(99_920)); + assertEquals("100K", StackFormatter.quantityToStackSize(100_000)); + assertEquals("10M", StackFormatter.quantityToStackSize(10_000_000)); + assertEquals(NumberFormat.getNumberInstance().format(2.14) + "B", StackFormatter.quantityToStackSize(Integer.MAX_VALUE)); + + assertEquals("0", StackFormatter.quantityToStackSize(-0)); + assertEquals("-400", StackFormatter.quantityToStackSize(-400)); + assertEquals("-400K", StackFormatter.quantityToStackSize(-400_000)); + assertEquals("-40M", StackFormatter.quantityToStackSize(-40_000_000)); + assertEquals(NumberFormat.getNumberInstance().format(-2.14) + "B", StackFormatter.quantityToStackSize(Integer.MIN_VALUE)); + } + + @Test + public void stackSizeToQuantity() throws ParseException + { + assertEquals(0, StackFormatter.stackSizeToQuantity("0")); + assertEquals(907, StackFormatter.stackSizeToQuantity("907")); + assertEquals(1200, StackFormatter.stackSizeToQuantity("1200")); + assertEquals(10_500, StackFormatter.stackSizeToQuantity(NumberFormat.getNumberInstance().format(10_500))); + assertEquals(10_500, StackFormatter.stackSizeToQuantity(NumberFormat.getNumberInstance().format(10.5) + "K")); + assertEquals(33_560_000, StackFormatter.stackSizeToQuantity(NumberFormat.getNumberInstance().format(33.56) + "M")); + assertEquals(2_000_000_000, StackFormatter.stackSizeToQuantity("2B")); + + assertEquals(0, StackFormatter.stackSizeToQuantity("-0")); + assertEquals(-400, StackFormatter.stackSizeToQuantity("-400")); + assertEquals(-400_000, StackFormatter.stackSizeToQuantity("-400k")); + assertEquals(-40_543_000, StackFormatter.stackSizeToQuantity(NumberFormat.getNumberInstance().format(-40.543) + "M")); + + try + { + StackFormatter.stackSizeToQuantity("0L"); + fail("Should have thrown an exception for invalid suffix."); + } + catch (ParseException ignore) + { + } + + try + { + StackFormatter.stackSizeToQuantity("badstack"); + fail("Should have thrown an exception for improperly formatted stack."); + } + catch (ParseException ignore) + { + } + } +} \ No newline at end of file From 326da42c1e1fde50f8299aea9363ca2eddcbf7b7 Mon Sep 17 00:00:00 2001 From: arlyon Date: Sun, 4 Mar 2018 23:21:35 +0000 Subject: [PATCH 2/2] Update plugins to use either NumberFormat or the StackFormatter --- .../blastfurnace/BlastFurnaceCofferOverlay.java | 6 ++---- .../plugins/chatcommands/ChatCommandsPlugin.java | 6 ++++-- .../client/plugins/examine/ExaminePlugin.java | 12 +++++++----- .../grandexchange/GrandExchangeOfferSlot.java | 5 ++++- .../plugins/grounditems/GroundItemsOverlay.java | 3 ++- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceCofferOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceCofferOverlay.java index 6846060ff1..3dfee32b69 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceCofferOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/blastfurnace/BlastFurnaceCofferOverlay.java @@ -27,7 +27,6 @@ package net.runelite.client.plugins.blastfurnace; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Point; -import java.text.NumberFormat; import javax.inject.Inject; import net.runelite.api.Client; import static net.runelite.api.Varbits.BLAST_FURNACE_COFFER; @@ -36,11 +35,10 @@ import net.runelite.api.widgets.WidgetInfo; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayPosition; import net.runelite.client.ui.overlay.components.PanelComponent; +import net.runelite.client.util.StackFormatter; class BlastFurnaceCofferOverlay extends Overlay { - private static final NumberFormat NUMBER_FORMATTER = NumberFormat.getInstance(); - private final Client client; private final BlastFurnacePlugin plugin; private final PanelComponent panelComponent = new PanelComponent(); @@ -71,7 +69,7 @@ class BlastFurnaceCofferOverlay extends Overlay panelComponent.getLines().add(new PanelComponent.Line( "Coffer:", - NUMBER_FORMATTER.format(client.getSetting(BLAST_FURNACE_COFFER)) + " gp" + StackFormatter.quantityToStackSize(client.getSetting(BLAST_FURNACE_COFFER)) + " gp" )); } return panelComponent.render(graphics, parent); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java index 207c02ff47..243f47c0a2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/chatcommands/ChatCommandsPlugin.java @@ -28,6 +28,7 @@ package net.runelite.client.plugins.chatcommands; import com.google.common.eventbus.Subscribe; import com.google.inject.Provides; import java.io.IOException; +import java.text.NumberFormat; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ScheduledExecutorService; @@ -64,6 +65,7 @@ import net.runelite.http.api.item.SearchResult; public class ChatCommandsPlugin extends Plugin { private static final float HIGH_ALCHEMY_CONSTANT = 0.6f; + private static final NumberFormat NUMBER_FORMATTER = NumberFormat.getInstance(); private final HiscoreClient hiscoreClient = new HiscoreClient(); @@ -247,7 +249,7 @@ public class ChatCommandsPlugin extends Plugin .append(ChatColorType.NORMAL) .append(": GE average ") .append(ChatColorType.HIGHLIGHT) - .append(String.format("%,d", itemPrice.getPrice())); + .append(NUMBER_FORMATTER.format(itemPrice.getPrice())); ItemComposition itemComposition = itemManager.getItemComposition(itemId); if (itemComposition != null) @@ -257,7 +259,7 @@ public class ChatCommandsPlugin extends Plugin .append(ChatColorType.NORMAL) .append(" HA value ") .append(ChatColorType.HIGHLIGHT) - .append(String.format("%,d", alchPrice)); + .append(NUMBER_FORMATTER.format(alchPrice)); } String response = builder.build(); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/examine/ExaminePlugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/examine/ExaminePlugin.java index deea6d8c60..2af25bfc1f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/examine/ExaminePlugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/examine/ExaminePlugin.java @@ -31,6 +31,7 @@ import com.google.common.cache.CacheBuilder; import com.google.common.eventbus.Subscribe; import com.google.inject.Provides; import java.io.IOException; +import java.text.NumberFormat; import java.time.Instant; import java.util.ArrayDeque; import java.util.Deque; @@ -72,6 +73,7 @@ import net.runelite.http.api.item.ItemPrice; public class ExaminePlugin extends Plugin { private static final float HIGH_ALCHEMY_CONSTANT = 0.6f; + private static final NumberFormat NUMBER_FORMATTER = NumberFormat.getInstance(); private final ExamineClient examineClient = new ExamineClient(); private final Deque pending = new ArrayDeque<>(); @@ -333,7 +335,7 @@ public class ExaminePlugin extends Plugin if (quantity > 1) { message - .append(String.format("%,d", quantity)) + .append(NUMBER_FORMATTER.format(quantity)) .append(" x "); } @@ -348,7 +350,7 @@ public class ExaminePlugin extends Plugin .append(ChatColorType.NORMAL) .append(" GE average ") .append(ChatColorType.HIGHLIGHT) - .append(String.format("%,d", gePrice * quantity)); + .append(NUMBER_FORMATTER.format(gePrice * quantity)); } if (quantity > 1) @@ -357,7 +359,7 @@ public class ExaminePlugin extends Plugin .append(ChatColorType.NORMAL) .append(" (") .append(ChatColorType.HIGHLIGHT) - .append(Integer.toString(gePrice)) + .append(NUMBER_FORMATTER.format(gePrice)) .append(ChatColorType.NORMAL) .append("ea)"); } @@ -368,7 +370,7 @@ public class ExaminePlugin extends Plugin .append(ChatColorType.NORMAL) .append(" HA value ") .append(ChatColorType.HIGHLIGHT) - .append(String.format("%,d", alchPrice * quantity)); + .append(NUMBER_FORMATTER.format(alchPrice * quantity)); } if (quantity > 1) @@ -377,7 +379,7 @@ public class ExaminePlugin extends Plugin .append(ChatColorType.NORMAL) .append(" (") .append(ChatColorType.HIGHLIGHT) - .append(Integer.toString(alchPrice)) + .append(NUMBER_FORMATTER.format(alchPrice)) .append(ChatColorType.NORMAL) .append("ea)"); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlot.java b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlot.java index ac3bde95cf..689ac1120f 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlot.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grandexchange/GrandExchangeOfferSlot.java @@ -28,6 +28,7 @@ package net.runelite.client.plugins.grandexchange; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; +import java.text.NumberFormat; import javax.annotation.Nullable; import javax.swing.BorderFactory; import javax.swing.Box; @@ -47,6 +48,8 @@ import net.runelite.client.game.ItemManager; @Slf4j public class GrandExchangeOfferSlot extends JPanel { + private static final NumberFormat NUMBER_FORMATTER = NumberFormat.getInstance(); + private static final Color GE_INPROGRESS_ORANGE = new Color(0xd8, 0x80, 0x20).brighter(); private static final Color GE_FINISHED_GREEN = new Color(0, 0x5f, 0); private static final Color GE_CANCELLED_RED = new Color(0x8f, 0, 0); @@ -141,7 +144,7 @@ public class GrandExchangeOfferSlot extends JPanel ImageIcon newItemIcon = new ImageIcon(itemManager.getImage(newOffer.getItemId(), newOffer.getTotalQuantity(), shouldStack)); itemIcon.setIcon(newItemIcon); - offerState.setText(getNameForState(newOffer.getState()) + " at " + newOffer.getPrice() + (newOffer.getTotalQuantity() > 1 ? "gp ea" : "gp")); + offerState.setText(getNameForState(newOffer.getState()) + " at " + NUMBER_FORMATTER.format(newOffer.getPrice()) + (newOffer.getTotalQuantity() > 1 ? "gp ea" : "gp")); progressBar.setMaximum(newOffer.getTotalQuantity()); progressBar.setValue(newOffer.getQuantitySold()); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java index 04a1a06cf0..424ab6ae67 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/grounditems/GroundItemsOverlay.java @@ -52,6 +52,7 @@ import net.runelite.client.ui.FontManager; import net.runelite.client.ui.overlay.Overlay; import net.runelite.client.ui.overlay.OverlayLayer; import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.util.StackFormatter; import net.runelite.http.api.item.ItemPrice; public class GroundItemsOverlay extends Overlay @@ -259,7 +260,7 @@ public class GroundItemsOverlay extends Overlay } itemStringBuilder.append(" (EX: ") - .append(ItemManager.quantityToStackSize(cost)) + .append(StackFormatter.quantityToStackSize(cost)) .append(" gp)"); }