Format.java

package eu.javaexperience.text;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Formatter;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Pattern;

import eu.javaexperience.interfaces.simple.SimpleGet;
import eu.javaexperience.resource.pool.SimplifiedResourcePool;

public class Format
{
	private Format() {}
	
	public static class DateFormatParseUnit
	{
		protected String format;
		protected TimeZone tz;
		
		protected SimpleDateFormat create(String format)
		{
			SimpleDateFormat ret = new SimpleDateFormat(format);
			if(null != tz)
			{
				ret.setTimeZone(tz);
			}
			return ret;
		}
		
		protected final SimplifiedResourcePool<SimpleDateFormat> pool = new SimplifiedResourcePool<>(new SimpleGet<SimpleDateFormat>()
		{
			@Override
			public SimpleDateFormat get()
			{
				return create(format);
			}
		});
		
		public DateFormatParseUnit(String format)
		{
			this.format = format;
		}
		
		public DateFormatParseUnit(String format, TimeZone tz)
		{
			this.format = format;
			this.tz = tz;
		}
		
		
		public String format(Date d)
		{
			SimpleDateFormat format = pool.acquireResource();
			try
			{
				return format.format(d);
			}
			finally
			{
				pool.releaseResource(format);
			}
		}
		
		public Date parse(String str) throws ParseException
		{
			SimpleDateFormat format = pool.acquireResource();
			try
			{
				return format.parse(str);
			}
			finally
			{
				pool.releaseResource(format);
			}
		}
		
		public Date tryParse(String date)
		{
			try
			{
				return parse(date);
			}
			catch(Exception e)
			{
				return null;
			}
		}

		public String getFormat()
		{
			return format;
		}
	}
	
	public static final DateFormatParseUnit SQL_TIMESTAMP_MS = new DateFormatParseUnit("yyyy-MM-dd HH:mm:ss.SSS");
	
	public static String sqlTimestampMilisec(Date d)
	{
		return SQL_TIMESTAMP_MS.format(d);
	}
	
	public static final DateFormatParseUnit UTC_SQL_TIMESTAMP_MS = new DateFormatParseUnit("yyyy-MM-dd HH:mm:ss.SSS'U'", TimeZone.getTimeZone("UTC"));
	
	public static final DateFormatParseUnit SQL_TIMESTAMP = new DateFormatParseUnit("yyyy-MM-dd HH:mm:ss");
	
	public static String sqlTimestamp(Date d)
	{
		return SQL_TIMESTAMP.format(d);
	}
	
	public static Date parseSqlTimestamp(String date) throws ParseException
	{
		return SQL_TIMESTAMP.parse(date);
	}
	
	public static final DateFormatParseUnit SQL_DATE = new DateFormatParseUnit("yyyy-MM-dd");
	
	public static Date tryParseSqlTimestamp(String date)
	{
		return SQL_TIMESTAMP.tryParse(date);
	}
	
	public static String formatSqlDate(Date d)
	{
		return SQL_DATE.format(d);
	}
	
	
	public static final DateFormatParseUnit UTC_FILE_SQL_TIMESTAMP = new DateFormatParseUnit("yyyy-MM-dd_HH-mm-ss_SSS'U'", TimeZone.getTimeZone("UTC"));
	
	public static final DateFormatParseUnit FILE_SQL_TIMESTAMP = new DateFormatParseUnit("yyyy-MM-dd_HH-mm-ss_SSS");
	
	public static final DateFormatParseUnit BROWSER_DATETIMESTAMP = new DateFormatParseUnit("yyyy-MM-dd'T'HH:mm");
	
	public static String shortenStringIfLongerThan(String str, int len)
	{
		if(str.length() > len)
		{
			return str.substring(0, len)+"...";
		}
		else
		{
			return str;
		}
	}
	
	private static class ReuseableFormatter
	{
		StringBuilder sb = new StringBuilder();
		Formatter f = new Formatter(sb);
	}
	
	private static final ThreadLocal<ReuseableFormatter> reuseableFormatter =  new ThreadLocal<ReuseableFormatter>()
	{
		@Override
		protected ReuseableFormatter initialValue()
		{
			return new ReuseableFormatter();
		}
	};
	
	public static String format(String format, Object... args)
	{
		ReuseableFormatter f = reuseableFormatter.get();
		f.f.format(format, args);
		String ret = f.sb.toString();
		f.sb.delete(0, f.sb.length());
		return ret;
	}
	
	public static String getPrintedStackTrace(Throwable t)
	{
		if(t == null)
			return null;
		
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		PrintStream ps = new PrintStream(baos);
		t.printStackTrace(ps);
		return new String(baos.toByteArray());
	}
	
	public static String stackTraceToString(StackTraceElement[] ele)
	{
		StringBuilder sb = new StringBuilder();
		for(StackTraceElement e:ele)
		{
			sb.append("\n\t");
			sb.append(e);
		}
		
		return sb.toString();
	}
	
	public static String cutFloatDecimals(String num, int decimals)
	{
		StringBuilder sb = new StringBuilder();
		boolean bp = true;
		int n = 0;
		for(int i=0;i<num.length();i++)
		{
			char c = num.charAt(i);
			if(c == '.')
			{
				bp = false;
				
				//lookahead
				int max = Math.min(decimals, num.length()-i-1);
				boolean need = false;
				for(int j=0;j < max;++j)
				{
					if(num.charAt(i+j+1) != '0')
					{
						need = true;
						break;
					}
				}
				
				if(!need)
				{
					break;
				}
				
				sb.append('.');
				
				continue;
			}
			
			if(bp)
			{
				sb.append(c);
			}
			else if(++n > decimals)
			{
				break;
			}
			else
			{
				sb.append(c);
			}
		}
		
		return sb.toString();
	}
	
	private static final ThreadLocal<DecimalFormat> DoubleFormat =  new ThreadLocal<DecimalFormat>()
	{
		@Override
		protected DecimalFormat initialValue()
		{
			return new DecimalFormat("#.##");
		}
	};
	
	public static String formatDouble(double n)
	{
		DecimalFormat df = DoubleFormat.get();
		return df.format(n).replace(',', '.');
	}

	public static String toHex(byte[] data)
	{
		byte[] re = new byte[data.length*2];
		for(int i=0;i<data.length;++i)
		{
			byte b = data[i];
			re[i*2] =  hexaTransform((0xf0 & b) >>> 4);
			re[i*2+1] = hexaTransform(0x0f & b);
		}
		return new String(re);
	}
	
	public static byte hexaTransform(int val)
	{
		if(val < 10)
		{
			return (byte) ('0'+ val);
		}
		else
		{
			return (byte) ('a'+ (val-10));
		}
	}
	
	public static int fromHexaTransform(char val)
	{
		if(val > '9')
		{
			return (byte) (10+(val-'a'));
		}
		return (byte) (val-'0');
	}
	
	public static byte[] fromHex(String hex)
	{
		hex = hex.toLowerCase();
		byte[] ret = new byte[hex.length()/2];
		
		for(int i=0;i<ret.length;++i)
		{
			ret[i] =  (byte) ( (fromHexaTransform(hex.charAt(i*2)) << 4)
					|
						fromHexaTransform(hex.charAt(i*2+1)) );
		}
		return ret;
	}
	
	private static final char[] CHAR_TABLE = {
		'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
		'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
		'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
		'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' };
	
	
	public static String base64Encode(byte[] data)
	{
		StringBuilder buffer = new StringBuilder();
		int pad = 0;
		for (int i = 0; i < data.length; i += 3) {

			int b = ((data[i] & 0xFF) << 16) & 0xFFFFFF;
			if (i + 1 < data.length) {
				b |= (data[i+1] & 0xFF) << 8;
			} else {
				pad++;
			}
			if (i + 2 < data.length) {
				b |= (data[i+2] & 0xFF);
			} else {
				pad++;
			}

			for (int j = 0; j < 4 - pad; j++) {
				int c = (b & 0xFC0000) >> 18;
				buffer.append(CHAR_TABLE[c]);
				b <<= 6;
			}
		}
		for (int j = 0; j < pad; j++) {
			buffer.append("=");
		}

		return buffer.toString();
	}

	private static final int[] BASE64_TABLE = {
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
		55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2,
		3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
		20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30,
		31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
		48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
		-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
	
	public static byte[] base64Decode(String data)
	{
		byte[] bytes = data.getBytes();
		ByteArrayOutputStream buffer = new ByteArrayOutputStream();
		for (int i = 0; i < bytes.length; ) {
			int b = 0;
			if (BASE64_TABLE[bytes[i]] != -1) {
				b = (BASE64_TABLE[bytes[i]] & 0xFF) << 18;
			}
			// skip unknown characters
			else {
				i++;
				continue;
			}

			int num = 0;
			if (i + 1 < bytes.length && BASE64_TABLE[bytes[i+1]] != -1) {
				b = b | ((BASE64_TABLE[bytes[i+1]] & 0xFF) << 12);
				num++;
			}
			if (i + 2 < bytes.length && BASE64_TABLE[bytes[i+2]] != -1) {
				b = b | ((BASE64_TABLE[bytes[i+2]] & 0xFF) << 6);
				num++;
			}
			if (i + 3 < bytes.length && BASE64_TABLE[bytes[i+3]] != -1) {
				b = b | (BASE64_TABLE[bytes[i+3]] & 0xFF);
				num++;
			}

			while (num > 0) {
				int c = (b & 0xFF0000) >> 16;
				buffer.write((char)c);
				b <<= 8;
				num--;
			}
			i += 4;
		}
		return buffer.toByteArray();
	}
	
	
	//https://stackoverflow.com/questions/1236678/phps-strtotime-in-java
	public static final class strtotime
	{
		private strtotime(){}
		
		private static final List<Matcher> matchers;

		static
		{
			matchers = new LinkedList<>();
			matchers.add(new NowMatcher());
			matchers.add(new TomorrowMatcher());
			
			ArrayList<String> stringFormats = new ArrayList<>(); 
			
			//to milisec
			stringFormats.add("yyyy-MM-dd HH:mm:ss.SSS");
			stringFormats.add("yyyy.MM.dd HH:mm:ss.SSS");
			stringFormats.add("yyyy.MM.dd. HH:mm:ss.SSS");
			stringFormats.add("yyyy-MM-dd_HH-mm-ss_SSS");
			stringFormats.add("yyyy. MM. dd. HH:mm:ss.SSS");
			
			//to sec with timezone
			
			stringFormats.add("EEE dd-MMM-yyyy HH:mm:ss z");
			stringFormats.add("EEE dd MMM yyyy HH:mm:ss zzz");
			stringFormats.add("yyyy.MM.dd G 'at' HH:mm:ss z");
			stringFormats.add("EEE d MMM yyyy HH:mm:ss Z");
			
			//000 => 111
			stringFormats.add("MM/dd/yyyy hh:mm:ss aaa");
			stringFormats.add("MM/dd/yyyy hh:mm:s aaa");
			stringFormats.add("MM/dd/yyyy hh:m:ss aaa");
			stringFormats.add("MM/dd/yyyy hh:m:s aaa");
			
			stringFormats.add("MM/dd/yyyy h:mm:ss aaa");
			stringFormats.add("MM/dd/yyyy h:mm:s aaa");
			stringFormats.add("MM/dd/yyyy h:m:ss aaa");
			stringFormats.add("MM/dd/yyyy h:m:s aaa");
			
			
			//to sec
			stringFormats.add("yyyy/MM/dd HH:mm:ss");
			stringFormats.add("yyyy. MM. dd HH:mm:ss");
			stringFormats.add("yyyy. MM. dd. HH:mm:ss");
			stringFormats.add("yyyy-MM-dd HH:mm:ss");
			stringFormats.add("yyyy.MM.dd HH:mm:ss");
			stringFormats.add("yyyy.MM.dd. HH:mm:ss");
			
			stringFormats.add("yyyy-MM-dd'T'HH:mm:ss");

			//to minutes
			stringFormats.add("yyyy-MM-dd'T'HH:mm");
			stringFormats.add("yyyy-MM-dd HH:mm");
			stringFormats.add("yyyy/MM/dd HH:mm");
			stringFormats.add("yyyy.MM.dd HH:mm");
			
			
			
			//YMD
			stringFormats.add("MM/dd/yyyy");
			stringFormats.add("yyyy/MM/dd");
			stringFormats.add("yyyy MM dd");
			stringFormats.add("yyyy. MM. dd");
			stringFormats.add("yyyy. MM. dd.");
			stringFormats.add("yyyy-MM-dd");
			stringFormats.add("yyyy.MM.dd");
			stringFormats.add("yyyy.MM.dd.");
			
			
			//DayofWeek, Month, day, year hour:min:sec			
			stringFormats.add("EEEE MMMM d yyyy hh:mm:ss");
			stringFormats.add("EEEE MMMM d yyyy hh:mm:s");
			stringFormats.add("EEEE MMMM d yyyy hh:m:ss");
			stringFormats.add("EEEE MMMM d yyyy hh:m:s");
			
			//Tuesday 30, April 2019
			stringFormats.add("EEEE d MMMM yyyy");
			
			
			
			for(String s:stringFormats)
			{
				registerMatcher(new DateFormatMatcher(new SimpleDateFormat(s)));
				registerMatcher(new DateFormatMatcher(new SimpleDateFormat(s, Locale.US)));
			}
			
			// add as many format as you want 
		}

		// not thread-safe
		public static void registerMatcher(Matcher matcher)
		{
			matchers.add(matcher);
		}

		public static interface Matcher
		{
			public Date tryConvert(String input);
		}

		private static class DateFormatMatcher implements Matcher
		{
			private final DateFormat dateFormat;

			public DateFormatMatcher(DateFormat dateFormat)
			{
				this.dateFormat = dateFormat;
			}

			public Date tryConvert(String input)
			{
				try
				{
					synchronized(dateFormat)
					{
						return dateFormat.parse(input);
					}
				}
				catch (ParseException ex)
				{
					return null;
				}
			}
		}

		private static class NowMatcher implements Matcher
		{
			private final Pattern now = Pattern.compile("now");

			public Date tryConvert(String input)
			{
				if(now.matcher(input).matches())
				{
					return new Date();
				}
				else
				{
					return null;
				}
			}
		}

		private static class TomorrowMatcher implements Matcher
		{
			private final Pattern tomorrow = Pattern.compile("tomorrow");

			public Date tryConvert(String input)
			{
				if(tomorrow.matcher(input).matches())
				{
					Calendar calendar = Calendar.getInstance();
					calendar.add(Calendar.DAY_OF_YEAR, +1);
					return calendar.getTime();
				}
				else
				{
					return null;
				}
			}
		}

		public static Date strtotime(String input)
		{
			if(null != input)
			{
				input = StringTools.replaceAllStrings(input, ",", "");
			}
			
			for(Matcher matcher:matchers)
			{
				Date date = matcher.tryConvert(input);
				if (date != null)
				{
					return date;
				}
			}

			return null;
		}
	}

	public static Date strToTime(String date)
	{
		return strtotime.strtotime(date);
	}
}