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