/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* 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 com.twelvemonkeys.util;
import com.twelvemonkeys.lang.StringUtil;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.StringTokenizer;
import java.util.Vector;
/**
* Format for converting and parsing time.
* <P>
* The format is expressed in a string as follows:
* <DL>
* <DD>m (or any multiple of m's)
* <DT>the minutes part (padded with 0's, if number has less digits than
* the number of m's)
* m -> 0,1,...,59,60,61,...
* mm -> 00,01,...,59,60,61,...
* <DD>s or ss
* <DT>the seconds part (padded with 0's, if number has less digits than
* the number of s's)
* s -> 0,1,...,59
* ss -> 00,01,...,59
* <DD>S
* <DT>all seconds (including the ones above 59)
* </DL>
* <P>
* May not handle all cases, and formats... ;-)
* Safest is: Always delimiters between the minutes (m) and seconds (s) part.
* <P>
* TODO:
* Move to com.twelvemonkeys.text?
* Milliseconds!
* Fix bugs.
* Known bugs:
* <P>
* The last character in the formatString is not escaped, while it should be.
* The first character after an escaped character is escaped while is shouldn't
* be.
* <P>
* This is not a 100% compatible implementation of a java.text.Format.
*
* @see com.twelvemonkeys.util.Time
*
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
*/
public class TimeFormat extends Format {
final static String MINUTE = "m";
final static String SECOND = "s";
final static String TIME = "S";
final static String ESCAPE = "\\";
/**
* The default time format
*/
private final static TimeFormat DEFAULT_FORMAT = new TimeFormat("m:ss");
protected String formatString = null;
/**
* Main method for testing ONLY
*/
static void main(String[] argv) {
Time time = null;
TimeFormat in = null;
TimeFormat out = null;
if (argv.length >= 3) {
System.out.println("Creating out TimeFormat: \"" + argv[2] + "\"");
out = new TimeFormat(argv[2]);
}
if (argv.length >= 2) {
System.out.println("Creating in TimeFormat: \"" + argv[1] + "\"");
in = new TimeFormat(argv[1]);
}
else {
System.out.println("Using default format for in");
in = DEFAULT_FORMAT;
}
if (out == null)
out = in;
if (argv.length >= 1) {
System.out.println("Parsing: \"" + argv[0] + "\" with format \""
+ in.formatString + "\"");
time = in.parse(argv[0]);
}
else
time = new Time();
System.out.println("Time is \"" + out.format(time) +
"\" according to format \"" + out.formatString + "\"");
}
/**
* The formatter array.
*/
protected TimeFormatter[] formatter;
/**
* Creates a new TimeFormat with the given formatString,
*/
public TimeFormat(String pStr) {
formatString = pStr;
Vector formatter = new Vector();
StringTokenizer tok = new StringTokenizer(pStr, "\\msS", true);
String previous = null;
String current = null;
int previousCount = 0;
while (tok.hasMoreElements()) {
current = tok.nextToken();
if (previous != null && previous.equals(ESCAPE)) {
// Handle escaping of s, S or m
current = ((current != null) ? current : "")
+ (tok.hasMoreElements() ? tok.nextToken() : "");
previous = null;
previousCount = 0;
}
// Skip over first,
// or if current is the same, increase count, and try again
if (previous == null || previous.equals(current)) {
previousCount++;
previous = current;
}
else {
// Create new formatter for each part
if (previous.equals(MINUTE))
formatter.add(new MinutesFormatter(previousCount));
else if (previous.equals(SECOND))
formatter.add(new SecondsFormatter(previousCount));
else if (previous.equals(TIME))
formatter.add(new SecondsFormatter(-1));
else
formatter.add(new TextFormatter(previous));
previousCount = 1;
previous = current;
}
}
// Add new formatter for last part
if (previous != null) {
if (previous.equals(MINUTE))
formatter.add(new MinutesFormatter(previousCount));
else if (previous.equals(SECOND))
formatter.add(new SecondsFormatter(previousCount));
else if (previous.equals(TIME))
formatter.add(new SecondsFormatter(-1));
else
formatter.add(new TextFormatter(previous));
}
// Debug
/*
for (int i = 0; i < formatter.size(); i++) {
System.out.println("Formatter " + formatter.get(i).getClass()
+ ": length=" + ((TimeFormatter) formatter.get(i)).digits);
}
*/
this.formatter = (TimeFormatter[])
formatter.toArray(new TimeFormatter[formatter.size()]);
}
/**
* DUMMY IMPLEMENTATION!!
* Not locale specific.
*/
public static TimeFormat getInstance() {
return DEFAULT_FORMAT;
}
/** DUMMY IMPLEMENTATION!! */
/* Not locale specific
public static TimeFormat getInstance(Locale pLocale) {
return DEFAULT_FORMAT;
}
*/
/** DUMMY IMPLEMENTATION!! */
/* Not locale specific
public static Locale[] getAvailableLocales() {
return new Locale[] {Locale.getDefault()};
}
*/
/** Gets the format string. */
public String getFormatString() {
return formatString;
}
/** DUMMY IMPLEMENTATION!! */
public StringBuffer format(Object pObj, StringBuffer pToAppendTo,
FieldPosition pPos) {
if (!(pObj instanceof Time)) {
throw new IllegalArgumentException("Must be instance of " + Time.class);
}
return pToAppendTo.append(format(pObj));
}
/**
* Formats the the given time, using this format.
*/
public String format(Time pTime) {
StringBuilder buf = new StringBuilder();
for (int i = 0; i < formatter.length; i++) {
buf.append(formatter[i].format(pTime));
}
return buf.toString();
}
/** DUMMY IMPLEMENTATION!! */
public Object parseObject(String pStr, ParsePosition pStatus) {
Time t = parse(pStr);
pStatus.setIndex(pStr.length()); // Not 100%
return t;
}
/**
* Parses a Time, according to this format.
* <p>
* Will bug on some formats. It's safest to always use delimiters between
* the minutes (m) and seconds (s) part.
*
*/
public Time parse(String pStr) {
Time time = new Time();
int sec = 0;
int min = 0;
int pos = 0;
int skip = 0;
boolean onlyUseSeconds = false;
for (int i = 0; (i < formatter.length)
&& (pos + skip < pStr.length()) ; i++) {
// Go to next offset
pos += skip;
if (formatter[i] instanceof MinutesFormatter) {
// Parse MINUTES
if ((i + 1) < formatter.length
&& formatter[i + 1] instanceof TextFormatter) {
// Skip until next format element
skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos);
// Error in format, try parsing to end
if (skip < 0)
skip = pStr.length();
}
else if ((i + 1) >= formatter.length) {
// Skip until end of string
skip = pStr.length();
}
else {
// Hope this is correct...
skip = formatter[i].digits;
}
// May be first char
if (skip > pos)
min = Integer.parseInt(pStr.substring(pos, skip));
}
else if (formatter[i] instanceof SecondsFormatter) {
// Parse SECONDS
if (formatter[i].digits == -1) {
// Only seconds (or full TIME)
if ((i + 1) < formatter.length
&& formatter[i + 1] instanceof TextFormatter) {
// Skip until next format element
skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos);
}
else if ((i + 1) >= formatter.length) {
// Skip until end of string
skip = pStr.length();
}
else {
// Cannot possibly know how long?
skip = 0;
continue;
}
// Get seconds
sec = Integer.parseInt(pStr.substring(pos, skip));
// System.out.println("Only seconds: " + sec);
onlyUseSeconds = true;
break;
}
else {
// Normal SECONDS
if ((i + 1) < formatter.length
&& formatter[i + 1] instanceof TextFormatter) {
// Skip until next format element
skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos);
}
else if ((i + 1) >= formatter.length) {
// Skip until end of string
skip = pStr.length();
}
else {
skip = formatter[i].digits;
}
// Get seconds
sec = Integer.parseInt(pStr.substring(pos, skip));
}
}
else if (formatter[i] instanceof TextFormatter) {
skip = formatter[i].digits;
}
}
// Set the minutes part if we should
if (!onlyUseSeconds)
time.setMinutes(min);
// Set the seconds part
time.setSeconds(sec);
return time;
}
}
/**
* The base class of TimeFormatters
*/
abstract class TimeFormatter {
int digits = 0;
abstract String format(Time t);
}
/**
* Formats the seconds part of the Time
*/
class SecondsFormatter extends TimeFormatter {
SecondsFormatter(int pDigits) {
digits = pDigits;
}
String format(Time t) {
// Negative number of digits, means all seconds, no padding
if (digits < 0) {
return Integer.toString(t.getTime());
}
// If seconds is more than digits long, simply return it
if (t.getSeconds() >= Math.pow(10, digits)) {
return Integer.toString(t.getSeconds());
}
// Else return it with leading 0's
//return StringUtil.formatNumber(t.getSeconds(), digits);
return StringUtil.pad("" + t.getSeconds(), digits, "0", true);
}
}
/**
* Formats the minutes part of the Time
*/
class MinutesFormatter extends TimeFormatter {
MinutesFormatter(int pDigits) {
digits = pDigits;
}
String format(Time t) {
// If minutes is more than digits long, simply return it
if (t.getMinutes() >= Math.pow(10, digits)) {
return Integer.toString(t.getMinutes());
}
// Else return it with leading 0's
//return StringUtil.formatNumber(t.getMinutes(), digits);
return StringUtil.pad("" + t.getMinutes(), digits, "0", true);
}
}
/**
* Formats text constant part of the Time
*/
class TextFormatter extends TimeFormatter {
String text = null;
TextFormatter(String pText) {
text = pText;
// Just to be able to skip over
if (pText != null) {
digits = pText.length();
}
}
String format(Time t) {
// Simply return the text
return text;
}
}