All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sappenin.utils.numbers.BigDecimalCodec Maven / Gradle / Ivy

There is a newer version: 1.1.2
Show newest version
/**
 * Copyright (C) 2014 Sappenin Inc. ([email protected])
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.sappenin.utils.numbers;

import java.math.BigDecimal;
import java.text.ParseException;

/**
 * Encodes numbers as strings in such a way that the lexicographic order of the
 * encoded strings is the same as the natural order of the original numbers. The
 * length of an encoded number is only slightly larger than the length of its
 * original number. Unlike other schemes, there is no limit to the size of
 * numbers which may be encoded.
 * 
 * @author ted stockwell
 * @author David Fuelling 
 * @see "https://raw.github.com/sappenin/objectify-utils/master/src/main/java/com/sappenin/objectify/translate/util/BigDecimalCodec.java"
 */
public class BigDecimalCodec
{

	/**
	 * 
	 * @param input
	 * @return
	 */
	public static final String decode(String input)
	{
		try
		{
			if (input == null)
			{
				return null;
			}
			if (input.length() <= 0)
			{
				return "";
			}
			return new Decoder(input)._output;
		}
		catch (ParseException e)
		{
			throw new RuntimeException("Failed to decode number:" + input, e);
		}
	}

	/**
	 * 
	 * @param input
	 * @return
	 */
	public static final BigDecimal decodeAsBigDecimal(String input)
	{
		try
		{
			if (input == null)
			{
				return null;
			}
			if (input.length() <= 0)
			{
				throw new RuntimeException("Internal Error: Cannot decode an empty String");
			}
			return new BigDecimal(new Decoder(input)._output);
		}
		catch (ParseException e)
		{
			throw new RuntimeException("Failed to decode number:" + input, e);
		}
	}

	/**
	 * 
	 * @param input
	 * @return
	 */
	public static final String encode(String input)
	{
		try
		{
			if (input == null)
			{
				return null;
			}
			if (input.length() <= 0)
			{
				return "";
			}
			return new Encoder(input)._output;
		}
		catch (ParseException e)
		{
			throw new RuntimeException("Failed to parse number:" + input, e);
		}
	}

	/**
	 * 
	 * @param decimal
	 * @return
	 */
	public static final String encode(BigDecimal decimal)
	{
		if (decimal == null)
		{
			return null;
		}
		return BigDecimalCodec.encode(decimal.toPlainString());
	}

	/**
	 * 
	 *
	 */
	static public class Encoder
	{

		private String _input;
		private int _position = 0;
		private int _end;
		private String _output = "";
		private boolean _isNegative = false;

		private Encoder(String input) throws ParseException
		{
			this._input = input;
			this._end = this._input.length();

			char c = this._input.charAt(this._position);
			if (c == '-')
			{
				this._input.charAt(this._position++);
				this._isNegative = true;
			}

			this.readNumberBeforeDecimal();
			if (this.readDecimalPoint())
			{
				this.readNumber(this._end - this._position);
			}
			this._output += this._isNegative ? '?' : '*';
		}

		/**
		 * 
		 * @return
		 * @throws ParseException
		 */
		private boolean readDecimalPoint() throws ParseException
		{
			if (this._end <= this._position)
			{
				return false;
			}
			char c = this._input.charAt(this._position++);
			if (c != '.')
			{
				this.throwParseException("Expected decimal point");
			}
			if (this._end <= this._position)
			{
				return false;
			}
			this._output += this._isNegative ? ':' : '.';
			return true;
		}

		/**
		 * 
		 * @throws ParseException
		 */
		private void readNumberBeforeDecimal() throws ParseException
		{
			char[] buffer = new char[this._input.length()];

			// read number until decimal point reached or end
			int i = 0;
			while (this._end > this._position)
			{
				char c = this._input.charAt(this._position++);
				if (('0' <= c) && (c <= '9'))
				{
					buffer[i++] = (char) (this._isNegative ? '0' + ('9' - c) : c);
				}
				else if (c == '.')
				{
					this._position--;
					break;
				}
			}

			// now figure out needed prefixes
			String prefix = "";
			int l = i;
			String unaryPrefix = this._isNegative ? "*" : "?";
			while (1 < l)
			{
				unaryPrefix += this._isNegative ? '*' : '?';
				String s = Integer.toString(l);
				if (this._isNegative)
				{
					char[] cs = s.toCharArray();
					for (int j = 0; j < cs.length; j++)
					{
						cs[j] = (char) (('0' + '9') - cs[j]);
					}
					s = new String(cs);
				}
				prefix = s + prefix;
				l = s.length();
			}

			this._output += unaryPrefix; // output unary prefix count
			this._output += prefix; // output prefixes
			this._output += new String(buffer, 0, i); // now output actual
														// number
		}

		/**
		 * 
		 * @param length
		 */
		private void readNumber(int length)
		{
			if (this._isNegative)
			{
				while (0 < length--)
				{
					this._output += (char) ('0' + ('9' - this._input.charAt(this._position++)));
				}
			}
			else
			{
				this._output += this._input.substring(this._position, this._position + length);
				this._position += length;
			}
		}

		/**
		 * 
		 * @param message
		 * @throws ParseException
		 */
		private void throwParseException(String message) throws ParseException
		{
			throw new ParseException(message, this._position);
		}
	}

	/**
	 *
	 */
	static public class Decoder
	{

		private String _input;
		private int _position = 0;
		private int _end;
		private String _output = "";
		private boolean _isNegative = false;

		/**
		 * 
		 * @param input
		 * @throws ParseException
		 */
		private Decoder(String input) throws ParseException
		{
			this._input = input;
			this._end = this._input.length();
			int lastChar = this._input.charAt(this._end - 1);
			while ((lastChar == '*') || (lastChar == '?') || (lastChar == ':') || (lastChar == '.'))
			{
				lastChar = this._input.charAt((--this._end) - 1);
			}

			char c = this._input.charAt(this._position);
			if (c == '*')
			{
				this._output += '-';
				this._isNegative = true;
			}
			else if (c != '?')
			{
				throw new ParseException("All encoded numbers must begin with either '?' or '*'", this._position);
			}

			this.readSequence();
			if (this.readDecimalPoint())
			{
				this.readNumber(this._end - this._position);
			}
		}

		/**
		 * 
		 * @return
		 * @throws ParseException
		 */
		private boolean readDecimalPoint() throws ParseException
		{
			if (this._end <= this._position)
			{
				return false;
			}
			char c = this._input.charAt(this._position++);
			if (c != (this._isNegative ? ':' : '.'))
			{
				throw new ParseException("Expected decimal point", this._position);
			}
			if (this._end <= this._position)
			{
				return false;
			}
			this._output += '.';
			return true;
		}

		/**
		 * 
		 * @throws ParseException
		 */
		private void readSequence() throws ParseException
		{
			int sequenceCount = 0;
			while (true)
			{
				int c = this._input.charAt(this._position++);
				if ((c == '*') || (c == '?'))
				{
					sequenceCount++;
				}
				else
				{
					this._position--;
					break;
				}
			}
			this.readNumberSequence(sequenceCount);
		}

		/**
		 * 
		 * @param sequenceCount
		 */
		private void readNumberSequence(int sequenceCount)
		{
			int prefixLength = 1;
			while (1 < sequenceCount--)
			{
				prefixLength = this.readPrefix(prefixLength);
			}
			this.readNumber(prefixLength);
		}

		/**
		 * 
		 * @param length
		 * @return
		 */
		private int readPrefix(int length)
		{
			String s;
			if (this._isNegative)
			{
				char[] cs = new char[length];
				int i = 0;
				while (0 < length--)
				{
					cs[i++] = (char) ('0' + ('9' - this._input.charAt(this._position++)));
				}
				s = new String(cs);
			}
			else
			{
				s = this._input.substring(this._position, this._position + length);
				this._position += length;
			}
			return Integer.parseInt(s);
		}

		/**
		 * 
		 * @param length
		 */
		private void readNumber(int length)
		{
			if (this._isNegative)
			{
				while (0 < length--)
				{
					this._output += (char) ('0' + ('9' - this._input.charAt(this._position++)));
				}
			}
			else
			{
				this._output += this._input.substring(this._position, this._position + length);
				this._position += length;
			}
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy