//
// @(#)Phase.java 4/2002
//
// Copyright 2002 Zachary DelProposto. All rights reserved.
// Use is subject to license terms.
//
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
// Or from http://www.gnu.org/
//
package dip.world;
import dip.misc.Utils;
import java.io.Serializable;
import java.io.Externalizable;
import java.io.IOException;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Iterator;
/**
*
* A Phase object represents when a turn takes place, and contains the
* year, game phase (PhaseType), and Season information.
* <p>
* Phase objects are mutable and comparable.
* <p>
* PhaseType and SeasonType objects may be compared with referential equality.
* (For example, "Phase.getSeasonType() == SeasonType.SPRING")
*
*/
public class Phase implements java.io.Serializable, Comparable
{
/**
*
*/
private static final long serialVersionUID = 3651316399336505136L;
// internal constants: describes ordering of phases
// Setup is independent of this ordering.
// ordering: (for a given year)
// spring movement, spring retreat, fall movement, fall retreat, fall adjustment
// both these constants correspond, and must have equal sizes
private static final SeasonType[] ORDER_SEASON = {SeasonType.SPRING, SeasonType.SPRING,
SeasonType.FALL, SeasonType.FALL, SeasonType.FALL};
private static final PhaseType[] ORDER_PHASE = {PhaseType.MOVEMENT, PhaseType.RETREAT,
PhaseType.MOVEMENT, PhaseType.RETREAT, PhaseType.ADJUSTMENT};
// formatter to always 4-digit format a year
private static final DecimalFormat YEAR_FORMAT = new DecimalFormat("0000");
// instance variables
protected final SeasonType seasonType;
protected final YearType yearType;
protected final PhaseType phaseType;
private transient int orderIdx; // set by readResolve() when object de-serialized
/**
* Create a new Phase.
*/
public Phase(SeasonType seasonType, int year, PhaseType phaseType)
{
this(seasonType, new YearType(year), phaseType);
}// Phase()
/**
* Create a new Phase.
*/
public Phase(SeasonType seasonType, YearType yearType, PhaseType phaseType)
{
if(seasonType == null || yearType == null || phaseType == null)
{
throw new IllegalArgumentException("invalid args");
}
this.orderIdx = deriveOrderIdx(seasonType, phaseType);
if(orderIdx == -1)
{
throw new IllegalArgumentException("invalid seasontype/phasetype combination");
}
this.seasonType = seasonType;
this.yearType = yearType;
this.phaseType = phaseType;
}// Phase()
/** Create a new Phase, given a known index */
protected Phase(YearType yt, int idx)
{
this.orderIdx = idx;
this.yearType = yt;
this.phaseType = ORDER_PHASE[idx];
this.seasonType = ORDER_SEASON[idx];
}// Phase()
/** Returns the year */
public int getYear() { return yearType.getYear(); }
/** Returns the YearType */
public YearType getYearType() { return yearType; }
/** Returns the PhaseType */
public PhaseType getPhaseType() { return phaseType; }
/** Returns the SeasonType */
public SeasonType getSeasonType() { return seasonType; }
/** Displays as a short String (e.g., F1902R) */
public String getBriefName()
{
StringBuffer sb = new StringBuffer(6);
sb.append(seasonType.getBriefName());
sb.append(YEAR_FORMAT.format(yearType.getYear()));
sb.append(phaseType.getBriefName());
return sb.toString();
}// getBriefName()
/** Displays the phase as a String */
public String toString()
{
StringBuffer sb = new StringBuffer(64);
sb.append(seasonType);
sb.append(", ");
sb.append(yearType);
sb.append(" (");
sb.append(phaseType);
sb.append(')');
return sb.toString();
}// toString()
/** Returns true if the two phases are equivalent. */
public boolean equals(Object obj)
{
Phase phase = (Phase) obj;
if( yearType.equals(phase.yearType)
&& seasonType.equals(phase.seasonType)
&& phaseType.equals(phase.phaseType) )
{
return true;
}
return false;
}// equals()
/**
* Compares the Phase to the given Phase object. Returns a negative, zero, or
* positive integer depending if the given Phase is less than, equal, or
* greater than (temporally) to this Phase.
*/
public int compareTo(Object obj)
{
Phase phase = (Phase) obj;
int result = 0;
// year is dominant
result = yearType.compareTo(phase.yearType);
if(result != 0)
{
return result;
}
// then season
result = seasonType.compareTo(phase.seasonType);
if(result != 0)
{
return result;
}
// finally, phase type.
return phaseType.compareTo(phase.phaseType);
}// compareTo()
/** Get the phase that would be after to the current phase */
public Phase getNext()
{
// advance the phase index by one, UNLESS we are over; then
// advance the year and reset.
int idx = orderIdx + 1;
idx = (idx > ORDER_SEASON.length - 1) ? 0 : idx;
YearType yt = (idx == 0) ? yearType.getNext() : yearType;
return new Phase(yt, idx);
}// getNext()
/** Get the phase that would come before the current phase */
public Phase getPrevious()
{
int idx = orderIdx - 1;
YearType yt = (idx < 0) ? yearType.getPrevious() : yearType;
idx = (idx < 0) ? (ORDER_SEASON.length - 1) : idx;
return new Phase(yt, idx);
}// getPrevious()
/** given season/phase, derive the order index. If we cannot, our index is -1. */
private int deriveOrderIdx(SeasonType st, PhaseType pt)
{
for(int i=0; i<ORDER_SEASON.length; i++)
{
if( ORDER_SEASON[i] == st
&& ORDER_PHASE[i] == pt )
{
return i;
}
}
return -1;
}// deriveOrderIdx()
/**
* Determines if this phase is valid. Not all PhaseType and
* SeasonType combinations are valid.
*/
public static boolean isValid(SeasonType st, PhaseType pt)
{
for(int i=0; i<ORDER_SEASON.length; i++)
{
if( ORDER_SEASON[i] == st
&& ORDER_PHASE[i] == pt )
{
return true;
}
}
return false;
}// isValid()
/**
* Determines the Phase from a String.
* <p>
* Expects input in the following form(s):
* <p>
* Season, Year (Phase)<br>
* Season, Year [Phase]<br>
* SYYYYP as a single 6-character token, e.g., F1900M = Fall 1900, Movement<br>
* <p>
* Whitespace: space, comma, colon, semicolon, [], (), tab, newline, return, quotes
* <p>
* The order is not important. If the combination is not valid (via isValid()), or if
* any Phase component cannot be parsed, a null value is returned. Note that this is very
* forgiving, but it does not allow any non-word tokens between what we look for.
*/
public static Phase parse(final String in)
{
SeasonType seasonType = null;
YearType yearType = null;
PhaseType phaseType = null;
// special case: 6 char token (commonly seen in Judge input)
// 'bc' years aren't allowed in 6 char tokens.
if(in.length() == 6)
{
// parse season & phase
seasonType = SeasonType.parse(in.substring(0,1));
yearType = YearType.parse(in.substring(1,5));
phaseType = PhaseType.parse(in.substring(5,6));
if(seasonType == null || yearType == null || phaseType == null)
{
return null;
}
}
else
{
// case conversion
String lcIn = in.toLowerCase();
// our token list (should be 3 or 4; whitespace/punctuation is ignored)
ArrayList tokList = new ArrayList(10);
// get all tokens, ignoring ANY whitespace or punctuation; StringTokenizer is ideal for this
StringTokenizer st = new StringTokenizer(lcIn, " ,:;[](){}-_|/\\\"\'\t\n\r", false);
while(st.hasMoreTokens())
{
tokList.add( st.nextToken() );
}
// not enough tokens (we need at least 3)
if(tokList.size() < 3)
{
return null;
}
// parse until we run out of things to parse
Iterator iter = tokList.iterator();
while(iter.hasNext())
{
String tok = (String) iter.next();
SeasonType tmpSeason = SeasonType.parse(tok);
seasonType = (tmpSeason == null) ? seasonType : tmpSeason;
PhaseType tmpPhase = PhaseType.parse(tok);
phaseType = (tmpPhase == null) ? phaseType : tmpPhase;
YearType tmpYear = YearType.parse(tok);
yearType = (tmpYear == null) ? yearType : tmpYear;
}
if(yearType == null || seasonType == null || phaseType == null)
{
return null;
}
// 'bc' token may be 'loose'. If so, we need to find it, as the
// YearType parser was fed only a single token (no whitespace)
// e.g., "1083 BC" won't be parsed right, but "1083bc" will be.
if(lcIn.indexOf("bc") >= 0 || lcIn.indexOf("b.c.") >= 0)
{
if(yearType.getYear() > 0)
{
yearType = new YearType(-yearType.getYear());
}
}
// check season-phase validity
if(!isValid(seasonType, phaseType))
{
return null;
}
}
return new Phase(seasonType, yearType, phaseType);
}// parse()
/**
* Returns a String array, in order, of valid season/phase combinations.
* <p>
* E.g.: Spring Move, or Spring Adjustment, etc.
*/
public static String[] getAllSeasonPhaseCombos()
{
String[] spCombos = new String[ORDER_SEASON.length];
for(int i=0; i<ORDER_SEASON.length; i++)
{
StringBuffer sb = new StringBuffer(64);
sb.append(ORDER_SEASON[i].toString());
sb.append(' ');
sb.append(ORDER_PHASE[i].toString());
spCombos[i] = sb.toString();
}
return spCombos;
}// getAllSeasonPhaseCombos()
/** Reconstitute a Phase object */
protected Object readResolve()
throws java.io.ObjectStreamException
{
this.orderIdx = deriveOrderIdx(this.seasonType, this.phaseType);
return this;
}// readResolve()
public int hashCode(){
int code = 0;
code += yearType.hashCode() * 10000;
code += seasonType.hashCode() * 100;
code += phaseType.hashCode();
return code;
}
/**
* Represents seasons
* <p>
* SeasonType constants should be used, rather than creating new SeasonType objects.
*
*/
public static class SeasonType implements Serializable, Comparable
{
/**
*
*/
private static final long serialVersionUID = 8755829601640636554L;
// always-accepted english constants for SeasonTypes
protected static final String CONST_SPRING = "SPRING";
protected static final String CONST_FALL = "FALL";
protected static final String CONST_SUMMER = "SUMMER";
protected static final String CONST_WINTER = "WINTER";
// internal constants
protected static final String IL8N_SPRING = "SEASONTYPE_SPRING";
protected static final String IL8N_FALL = "SEASONTYPE_FALL";
// positions are spaced such that other seasons can be inserted easily
protected static final int POS_SPRING = 10;
protected static final int POS_FALL = 20;
// Season Type Constants
/** Spring season */
public static final SeasonType SPRING = new SeasonType(IL8N_SPRING, POS_SPRING);
/** Fall season */
public static final SeasonType FALL = new SeasonType(IL8N_FALL, POS_FALL);
/** SeasonType array
* <b>Warning: this should not be mutated.</b>
*/
public static final SeasonType[] ALL = { SPRING, FALL };
// instance variables
protected final int position;
protected transient String displayName = null;
/**
* Creates a new SeasonType
*/
protected SeasonType(String il8nKey, int pos)
{
this.position = pos;
setDisplayName(il8nKey);
}// SeasonType()
/** Return the name of this season */
public String toString()
{
return displayName;
}// toString()
/** Brief name of a Season (e.g., [F]all, [S]pring) */
public String getBriefName()
{
if(this == SPRING)
{
return "S";
}
else if(this == FALL)
{
return "F";
}
return "?";
}// getBriefName()
/** Returns the hashCode */
public int hashCode()
{
return position;
}// hashCode()
/** Returns <code>true</code> if SeasonType objects are equivalent */
public boolean equals(Object obj)
{
if(obj == this)
{
return true;
}
if(obj instanceof SeasonType)
{
return ( this.position == ((SeasonType)obj).position );
}
return false;
}// equals()
/**
* Compares the order of two seasons.
* <p>
* Fall always follows Spring.
*/
public int compareTo(Object obj)
{
SeasonType st = (SeasonType) obj;
return (position - st.position);
}// compareTo()
/** Get the next season */
public SeasonType getNext()
{
if(this == SPRING)
{
return FALL;
}
else if(this == FALL)
{
return SPRING;
}
return null;
}// getNext()
/** Get the previous season */
public SeasonType getPrevious()
{
if(this == SPRING)
{
return FALL;
}
else if(this == FALL)
{
return SPRING;
}
return null;
}// getPrevious()
/**
* Parse input to determine season; return null if input cannot be parsed
* into a known SeasonType constant.
* <p>
* Note: SUMMER and WINTER are converted to Spring and Fall, respectively.
*/
public static SeasonType parse(final String in)
{
// short cases (1 letter); not i18n'd
if(in.length() == 1)
{
String lcIn = in.toLowerCase();
if("s".equals(lcIn))
{
return SPRING;
}
else if("f".equals(lcIn) || ("w".equals(lcIn)))
{
return FALL;
}
return null;
}
// typical cases
if(in.equalsIgnoreCase(CONST_SPRING))
{
return SPRING;
}
else if(in.equalsIgnoreCase(CONST_FALL))
{
return FALL;
}
else if(in.equalsIgnoreCase(CONST_SUMMER))
{
return SPRING;
}
else if(in.equalsIgnoreCase(CONST_WINTER))
{
return FALL;
}
// il8n cases
if(in.equalsIgnoreCase(Utils.getLocalString(IL8N_SPRING)))
{
return SPRING;
}
else if(in.equalsIgnoreCase(Utils.getLocalString(IL8N_FALL)))
{
return FALL;
}
return null;
}// parse()
/** Resolves the serialized reference into a constant */
protected Object readResolve()
throws java.io.ObjectStreamException
{
SeasonType st = null;
if(position == POS_SPRING)
{
st = SPRING;
st.setDisplayName(IL8N_SPRING);
}
else if(position == POS_FALL)
{
st = FALL;
st.setDisplayName(IL8N_FALL);
}
return st;
}// readResolve()
/** Sets the internationalized name */
private void setDisplayName(String key)
{
this.displayName = Utils.getLocalString(key);
}// resetDisplayName()
}// nested class SeasonType
/**
* PhaseTypes represent game phases. For example, MOVEMENT or RETREAT phases.
* <p>
* PhaseType constants should be used instead of creating new PhaseType objects.
*
*/
public static class PhaseType implements Serializable, Comparable
{
/**
*
*/
private static final long serialVersionUID = -274023424634001759L;
// always-accepted english constants for phase types
// these MUST be in lower case
protected static final String CONST_ADJUSTMENT = "adjustment";
protected static final String CONST_MOVEMENT = "movement";
protected static final String CONST_RETREAT = "retreat";
// internal constants
protected static final String IL8N_ADJUSTMENT = "PHASETYPE_ADJUSTMENT";
protected static final String IL8N_MOVEMENT = "PHASETYPE_MOVEMENT";
protected static final String IL8N_RETREAT = "PHASETYPE_RETREAT";
// position constants
protected static final int POS_MOVEMENT = 10;
protected static final int POS_RETREAT = 20;
protected static final int POS_ADJUSTMENT = 30;
// PhaseType Constants
/** Adjustment PhaseType */
public static final PhaseType ADJUSTMENT = new PhaseType(CONST_ADJUSTMENT, POS_ADJUSTMENT, IL8N_ADJUSTMENT);
/** Movement PhaseType */
public static final PhaseType MOVEMENT = new PhaseType(CONST_MOVEMENT, POS_MOVEMENT, IL8N_MOVEMENT);
/** Retreat PhaseType */
public static final PhaseType RETREAT = new PhaseType(CONST_RETREAT, POS_RETREAT, IL8N_RETREAT);
/**
* PhaseType array
* <b>Warning: this should not be mutated.</b>
*/
public static final PhaseType[] ALL = { MOVEMENT, RETREAT, ADJUSTMENT };
// instance variables
protected transient String displayName = null;
protected final String constName;
protected final int position;
/** Create a new PhaseType */
protected PhaseType(String cName, int pos, String il8nKey)
{
this.constName = cName;
this.position = pos;
this.displayName = Utils.getLocalString(il8nKey);
}// PhaseType()
/** Get the name of a phase */
public String toString()
{
return displayName;
}// toString()
/** Brief name of a Phase (e.g., [B]uild, [R]etreat [M]ove */
public String getBriefName()
{
if(this == ADJUSTMENT)
{
return "B";
}
else if(this == RETREAT)
{
return "R";
}
else if(this == MOVEMENT)
{
return "M";
}
// unknown!
return "?";
}// getBriefName()
/** Returns the hashCode */
public int hashCode()
{
return position;
}// hashCode()
/** Returns <code>true</code> if PhaseType objects are equivalent */
public boolean equals(Object obj)
{
if(obj == this)
{
return true;
}
else if(obj instanceof PhaseType)
{
return ( this.position == ((PhaseType)obj).position );
}
return false;
}// equals()
/** Temporally compares PhaseType objects */
public int compareTo(Object obj)
{
PhaseType pt = (PhaseType) obj;
return (position - pt.position);
}// compareTo()
/**
* Get the next PhaseType, in sequence.
*/
public PhaseType getNext()
{
if(this == ADJUSTMENT)
{
return MOVEMENT;
}
else if(this == RETREAT)
{
return ADJUSTMENT;
}
else if(this == MOVEMENT)
{
return RETREAT;
}
return null;
}// getNext()
/**
* Get the previous PhaseType, in sequence.
*/
public PhaseType getPrevious()
{
if(this == ADJUSTMENT)
{
return RETREAT;
}
else if(this == RETREAT)
{
return MOVEMENT;
}
else if(this == MOVEMENT)
{
return ADJUSTMENT;
}
return null;
}// getPrevious()
/**
* Returns the appropriate PhaseType constant representing
* the input, or null.
* <p>
* Plurals are allowable on constants, but not in il8n versions.
*/
public static PhaseType parse(final String in)
{
// short cases (1 letter); not i18n'd
if(in.length() == 1)
{
String lcIn = in.toLowerCase();
if("m".equals(lcIn))
{
return MOVEMENT;
}
else if("a".equals(lcIn) || "b".equals(lcIn))
{
return ADJUSTMENT;
}
else if("r".equals(lcIn))
{
return RETREAT;
}
return null;
}
// typical cases; use 'startsWith'
if(in.startsWith(CONST_ADJUSTMENT))
{
return ADJUSTMENT;
}
else if(in.startsWith(CONST_MOVEMENT))
{
return MOVEMENT;
}
else if(in.startsWith(CONST_RETREAT))
{
return RETREAT;
}
// il8n cases
if(in.equalsIgnoreCase(Utils.getLocalString(IL8N_ADJUSTMENT)))
{
return ADJUSTMENT;
}
else if(in.equalsIgnoreCase(Utils.getLocalString(IL8N_MOVEMENT)))
{
return MOVEMENT;
}
else if(in.equalsIgnoreCase(Utils.getLocalString(IL8N_RETREAT)))
{
return RETREAT;
}
return null;
}// parse()
/** Resolves a serialized Phase object into a constant reference */
protected Object readResolve()
throws java.io.ObjectStreamException
{
PhaseType pt = null;
if(constName.equalsIgnoreCase(CONST_ADJUSTMENT))
{
pt = ADJUSTMENT;
pt.displayName = Utils.getLocalString(IL8N_ADJUSTMENT);
}
else if(constName.equalsIgnoreCase(CONST_MOVEMENT))
{
pt = MOVEMENT;
pt.displayName = Utils.getLocalString(IL8N_MOVEMENT);
}
else if(constName.equalsIgnoreCase(CONST_RETREAT))
{
pt = RETREAT;
pt.displayName = Utils.getLocalString(IL8N_RETREAT);
}
return pt;
}// readResolve()
}// nested class PhaseType
/**
* YearType is used to represent the Year
* <p>
* A YearType is used because we now support negative years ("BC")
* and need to appropriately advance, parse, and format these years.
* <p>
* A YearType is an immutable object.
*/
public static class YearType implements Serializable, Comparable
{
/**
*
*/
private static final long serialVersionUID = -602034791661770642L;
// instance fields
protected final int year;
/** Create a new YearType */
public YearType(int value)
{
if(value == 0)
{
throw new IllegalArgumentException("Year 0 not valid");
}
year = value;
}// YearType()
/** Get the name of a year. */
public String toString()
{
if(year >= 1000)
{
return String.valueOf(year);
}
else if(year > 0)
{
// explicitly add "AD"
StringBuffer sb = new StringBuffer(8);
sb.append(year);
sb.append(" AD");
return sb.toString();
}
else
{
StringBuffer sb = new StringBuffer(8);
sb.append(-year);
sb.append(" BC");
return sb.toString();
}
}// toString()
/** Gets the year. This will return a negative number if it is a BC year. */
public int getYear()
{
return year;
}// getYear()
/** Returns the hashcode */
public int hashCode()
{
return year;
}// hashCode()
/** Returns <code>true</code> if YearTYpe objects are equivalent */
public boolean equals(Object obj)
{
if(obj == this)
{
return true;
}
else if(obj instanceof YearType)
{
return (year == ((YearType) obj).year);
}
return false;
}// equals()
/** Temporally compares YearType objects */
public int compareTo(Object obj)
{
return (year - ((YearType) obj).year);
}// compareTo()
/**
* Get the next YearType, in sequence
*/
public YearType getNext()
{
// 1BC -> 1AD, otherwise, just add 1
return ((year == -1) ? new YearType(1) : new YearType(year + 1));
}// getNext()
/**
* Get the previous YearType, in sequence.
*/
public YearType getPrevious()
{
// 1 AD -> 1 BC, otherwise, just subtract 1
return ((year == 1) ? new YearType(-1) : new YearType(year - 1));
}// getPrevious()
/**
* Returns the appropriate YearType constant representing
* the input, or null.
* <p>
* 0 is not a valid year<br>
* Negative years are interpreted as BC<br>
* The modifier "BC" following a year is valid<br>
* A negative year with the BC modifier is still a BC year<br>
* Periods are NOT allowed in "BC"<br>
* The modifier BC must be in lower case<br>
*/
public static YearType parse(final String input)
{
String in = input;
final int idx = in.indexOf("bc");
boolean isBC = false;
if(idx >= 1)
{
isBC = true;
in = in.substring(0, idx);
}
int y = 0;
try
{
y = Integer.parseInt(in.trim());
}
catch(NumberFormatException e)
{
return null;
}
if(y == 0)
{
return null;
}
else if(y > 0 && isBC)
{
y = -y;
}
return new YearType(y);
}// parse()
}// nested class YearType
}// class Phase