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
This commit is contained in:
arlyon
2018-03-04 23:21:14 +00:00
committed by Adam
parent 4887e0ec2b
commit a098965d4c
3 changed files with 289 additions and 21 deletions

View File

@@ -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
*

View File

@@ -0,0 +1,183 @@
/*
* Copyright (c) 2018, arlyon <https://github.com/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;
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (c) 2018, arlyon <https://github.com/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)
{
}
}
}