<commit_before>/*
 * eXist Open Source Native XML Database
 * Copyright (C) 2001-2007 The eXist Project
 * http://exist-db.org
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *  
 *  $Id$
 */
package org.exist.xquery.value;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.Collator;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;

import org.apache.xerces.util.DatatypeMessageFormatter;
import org.exist.xquery.Constants;
import org.exist.xquery.ErrorCodes;
import org.exist.xquery.XPathException;

/**
 * @author wolf
 * @author <a href="mailto:<EMAIL>"><NAME></a>
 * @author ljo
 */
public abstract class AbstractDateTimeValue extends ComputableValue {
	
	//Provisionally public
	public final XMLGregorianCalendar calendar;
	private XMLGregorianCalendar implicitCalendar, canonicalCalendar, trimmedCalendar;
	
	protected static Pattern negativeDateStart = Pattern.compile("^\\d\\d?-(\\d+)-(.*)"); 

	public final static int YEAR = 0;
	public final static int MONTH = 1;
	public final static int DAY = 2;
	public final static int HOUR = 3;
	public final static int MINUTE = 4;
	public final static int SECOND = 5;
	public final static int MILLISECOND = 6;

	/**
	 * Create a new date time value based on the given calendar.  The calendar is
	 * <em>not</em> cloned, so it is the subclass's responsibility to make sure there are
	 * no external references to it that would allow for mutation.
	 *
	 * @param calendar the calendar to wrap into an XPath value
	 */
	protected AbstractDateTimeValue(XMLGregorianCalendar calendar) {
		this.calendar = calendar;
	}
	
	protected AbstractDateTimeValue(String lexicalValue) throws XPathException {
        lexicalValue = StringValue.trimWhitespace(lexicalValue);

        //lexicalValue = normalizeDate(lexicalValue);
        //lexicalValue = normalizeTime(getType(), lexicalValue);
		try {
			calendar = parse(lexicalValue);
		} catch (IllegalArgumentException e) {
			throw new XPathException(ErrorCodes.FORG0001, "illegal lexical form for date-time-like value '" + lexicalValue + "' " + e.getMessage(), e);
		}
	}
	
	/**
	 * Return a calendar with the timezone field set, to be used for order comparison.
	 * If the original calendar did not specify a timezone, set the local timezone (unadjusted
	 * for daylight savings).  The returned calendars will be totally ordered between themselves.
	 * We also set any missing fields to ensure that normalization doesn't discard important data!
	 * (This is probably a bug in the JAXP implementation, but the workaround doesn't hurt us,
	 * so it's faster to just fix it here.)
	 *
	 * @return the calendar represented by this object, with the timezone field filled in with an implicit value if necessary
	 */
	protected XMLGregorianCalendar getImplicitCalendar() {
		if (implicitCalendar == null) {
			implicitCalendar = (XMLGregorianCalendar) calendar.clone();
			if (calendar.getTimezone() == DatatypeConstants.FIELD_UNDEFINED) {
				implicitCalendar.setTimezone(TimeUtils.getInstance().getLocalTimezoneOffsetMinutes());
			}
			// fill in fields from default reference; don't have to worry about weird combinations of fields being set, since we control that on creation
			switch(getType()) {
				case Type.DATE: implicitCalendar.setTime(0,0,0); break;
				case Type.TIME: implicitCalendar.setYear(1972); implicitCalendar.setMonth(12); implicitCalendar.setDay(31); break;
				default:
			}
			implicitCalendar = implicitCalendar.normalize();	// the comparison routines will normalize it anyway, just do it once here
		}
		return implicitCalendar;
	}
	
	// TODO: method not currently used, apparently the XPath spec never needs to canonicalize
	// date/times after all (see section 17.1.2 on casting)
	protected XMLGregorianCalendar getCanonicalCalendar() {
		if (canonicalCalendar == null) {
			canonicalCalendar = getTrimmedCalendar().normalize();
		}
		return canonicalCalendar;
	}
	
	protected XMLGregorianCalendar getTrimmedCalendar() {
		if (trimmedCalendar == null) {
			trimmedCalendar = cloneXMLGregorianCalendar(calendar);
			BigDecimal fract = trimmedCalendar.getFractionalSecond();
			if (fract != null) {
				// TODO: replace following algorithm in JDK 1.5 with fract.stripTrailingZeros();
				String s = fract.toString();
				int i = s.length();
				while (i > 0 && s.charAt(i-1) == '0') i--;
				if (i == 0) trimmedCalendar.setFractionalSecond(null);
				else if (i != s.length()) trimmedCalendar.setFractionalSecond(new BigDecimal(s.substring(0, i)));
			}
		}
		return trimmedCalendar;
	}
	
	protected abstract AbstractDateTimeValue createSameKind(XMLGregorianCalendar cal) throws XPathException;
	
	protected long getTimeInMillis() {
		// use getImplicitCalendar() rather than relying on toGregorianCalendar timezone defaulting
		// to maintain consistency
		return getImplicitCalendar().toGregorianCalendar().getTimeInMillis();
	}
	
	protected abstract QName getXMLSchemaType();
	
	public String getStringValue() throws XPathException {
		String r = getTrimmedCalendar().toXMLFormat();
		// hacked to match the format mandated in XPath 2 17.1.2, which is different from the XML Schema canonical format
		//if (r.charAt(r.length()-1) == 'Z') r = r.substring(0, r.length()-1) + "+00:00";
		
		//Let's try these lexical transformations...		
		boolean startsWithDashDash = r.startsWith("--");
		r = r.replaceAll("--", "");
		if (startsWithDashDash)
			r = "--" + r;
		
		Matcher m = negativeDateStart.matcher(r);
		if (m.matches()) {
			int year = Integer.valueOf(m.group(1)).intValue();
			DecimalFormat df = new DecimalFormat("0000");
			r = "-" + df.format(year) + "-" + m.group(2);
		}
		
		return r;
	}

	public boolean effectiveBooleanValue() throws XPathException {
		throw new XPathException(ErrorCodes.FORG0006, "effective boolean value invalid operand type: " + Type.getTypeName(getType()));
	}
	
	public abstract AtomicValue convertTo(int requiredType) throws XPathException;

	public int getPart(int part) {
		switch (part) {
			case YEAR: return calendar.getYear(); 
			case MONTH: return calendar.getMonth();
			case DAY: return calendar.getDay();
			case HOUR: return calendar.getHour();
			case MINUTE: return calendar.getMinute();
			case SECOND: return calendar.getSecond();
			case MILLISECOND: int mSec=calendar.getMillisecond();
                                          if(mSec == DatatypeConstants.FIELD_UNDEFINED)
                                              return 0;
                                          else
                                              return calendar.getMillisecond();
			default: throw new IllegalArgumentException("Invalid argument to method getPart");
		}
	}
	
	private static final Duration tzLowerBound = TimeUtils.getInstance().newDurationDayTime("-PT14H");
	private static final Duration tzUpperBound = tzLowerBound.negate();
	protected void validateTimezone(DayTimeDurationValue offset) throws XPathException {
		Duration tz = offset.duration;
		Number secs = tz.getField(DatatypeConstants.SECONDS);
		if (secs != null && ((BigDecimal) secs).compareTo(BigDecimal.valueOf(0)) != 0)
			throw new XPathException(ErrorCodes.FODT0003, "duration " + offset + " has fractional minutes so cannot be used as a timezone offset");
		if (! (
				tz.equals(tzLowerBound) ||
				tz.equals(tzUpperBound) ||
				(tz.isLongerThan(tzLowerBound) && tz.isShorterThan(tzUpperBound))
			))
			throw new XPathException(ErrorCodes.FODT0003, "duration " + offset + " outside valid timezone offset range");
	}

	public AbstractDateTimeValue adjustedToTimezone(DayTimeDurationValue offset) throws XPathException {
		if (offset == null) offset = new DayTimeDurationValue(TimeUtils.getInstance().getLocalTimezoneOffsetMillis());
		validateTimezone(offset);
		XMLGregorianCalendar xgc = (XMLGregorianCalendar) calendar.clone();
		if (xgc.getTimezone() != DatatypeConstants.FIELD_UNDEFINED) {
			if (getType() == Type.DATE) xgc.setTime(0,0,0);	// set the fields so we don't lose precision when shifting timezones
			xgc = xgc.normalize();
			xgc.add(offset.duration);
		}
		try {
			xgc.setTimezone((int) (offset.getValue()/60));
		} catch (IllegalArgumentException e) {
			throw new XPathException(ErrorCodes.FORG0001, "illegal timezone offset " + offset, e);
		}
		return createSameKind(xgc);
	}
	
	public AbstractDateTimeValue withoutTimezone() throws XPathException {
		XMLGregorianCalendar xgc = (XMLGregorianCalendar) calendar.clone();
		xgc.setTimezone(DatatypeConstants.FIELD_UNDEFINED);
		return createSameKind(xgc);
	}
	
	public Sequence getTimezone() throws XPathException {
		int tz = calendar.getTimezone();
		if (tz == DatatypeConstants.FIELD_UNDEFINED) return Sequence.EMPTY_SEQUENCE;
		return new DayTimeDurationValue(tz * 60000L);
	}
	
	public boolean compareTo(Collator collator, int operator, AtomicValue other) throws XPathException {
		int cmp = compareTo(collator, other);
		switch (operator) {
			case Constants.EQ: return cmp == 0;
			case Constants.NEQ: return cmp != 0;
			case Constants.LT: return cmp < 0;
			case Constants.LTEQ: return cmp <= 0;
			case Constants.GT: return cmp > 0;
			case Constants.GTEQ: return cmp >= 0;
			default :
				throw new XPathException("Unknown operator type in comparison");
		}
	}

	public int compareTo(Collator collator, AtomicValue other) throws XPathException {
		if (other.getType() == getType()) {
			// filling in missing timezones with local timezone, should be total order as per XPath 2.0 10.4
			int r =  getImplicitCalendar().compare(((AbstractDateTimeValue) other).getImplicitCalendar());
			if (r == DatatypeConstants.INDETERMINATE) throw new RuntimeException("indeterminate order between " + this + " and " + other);
			return r;
		}
		throw new XPathException(
			"Type error: cannot compare " + Type.getTypeName(getType()) + " to "
				+ Type.getTypeName(other.getType()));
	}

	public AtomicValue max(Collator collator, AtomicValue other) throws XPathException {
		AbstractDateTimeValue otherDate = other.getType() == getType() ? (AbstractDateTimeValue) other : (AbstractDateTimeValue) other.convertTo(getType());
		return getImplicitCalendar().compare(otherDate.getImplicitCalendar()) > 0 ? this : other;
	}

	public AtomicValue min(Collator collator, AtomicValue other) throws XPathException {
		AbstractDateTimeValue otherDate = other.getType() == getType() ? (AbstractDateTimeValue) other : (AbstractDateTimeValue) other.convertTo(getType());
		return getImplicitCalendar().compare(otherDate.getImplicitCalendar()) < 0 ? this : other;
	}

	// override for xs:time
	public ComputableValue plus(ComputableValue other) throws XPathException {
		switch(other.getType()) {
			case Type.YEAR_MONTH_DURATION:
			case Type.DAY_TIME_DURATION:
				return other.plus(this);
			default:
				throw new XPathException(
						"Operand to plus should be of type xdt:dayTimeDuration or xdt:yearMonthDuration; got: "
							+ Type.getTypeName(other.getType()));
		}
	}

	public ComputableValue mult(ComputableValue other) throws XPathException {
		throw new XPathException("multiplication is not supported for type " + Type.getTypeName(getType()));
	}

	public ComputableValue div(ComputableValue other) throws XPathException {
		throw new XPathException("division is not supported for type " + Type.getTypeName(getType()));
	}

	public int conversionPreference(Class<?> javaClass) {
		if (javaClass.isAssignableFrom(DateValue.class)) return 0;
		if (javaClass.isAssignableFrom(XMLGregorianCalendar.class)) return 1;
		if (javaClass.isAssignableFrom(GregorianCalendar.class)) return 2;
		if (javaClass == Date.class) return 3;
		return Integer.MAX_VALUE;
	}

	public Object toJavaObject(Class<?> target) throws XPathException {
		if (target == Object.class || target.isAssignableFrom(DateValue.class))
			return this;
		else if (target.isAssignableFrom(XMLGregorianCalendar.class))
			return calendar.clone();
		else if (target.isAssignableFrom(GregorianCalendar.class))
			return calendar.toGregorianCalendar();
		else if (target == Date.class)
			return calendar.toGregorianCalendar().getTime();

		throw new XPathException("cannot convert value of type " + Type.getTypeName(getType()) + " to Java object of type " + target.getName());
	}
    
    /* (non-Javadoc)
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    public int compareTo(Object o)
    {
        if (o instanceof AbstractDateTimeValue) {
			AbstractDateTimeValue dt = (AbstractDateTimeValue) o;
    		return calendar.compare(dt.calendar);
		}

        final AtomicValue other = (AtomicValue)o;
        if(Type.subTypeOf(other.getType(), Type.DATE_TIME))
        	try {
        		//TODO : find something that will consume less resources
        		return calendar.compare(TimeUtils.getInstance().newXMLGregorianCalendar(other.getStringValue()));
        	} catch (XPathException e) {
        		System.out.println("Failed to get string value of '" + other + "'");
        		//Why not ?
        		return Constants.SUPERIOR;
        	}
        else
            return getType() > other.getType() ? Constants.SUPERIOR : Constants.INFERIOR;
    }	

    /**
     * Utility method that is able to clone a calendar whose year is 0
     * (whatever a year 0 means). 
     * It looks like the JDK is unable to do that.
     * @param calendar The Calendar to clone
     * @return the cloned Calendar
     */
    public static XMLGregorianCalendar cloneXMLGregorianCalendar(XMLGregorianCalendar calendar) {
    	boolean hacked = false;
		if (calendar.getYear() == 0) {
			calendar.setYear(1);
			hacked = true;
		}
		XMLGregorianCalendar result = (XMLGregorianCalendar)calendar.clone();
		if (hacked) {
			//reset everything
			calendar.setYear(0);
			//-1 could also be considered
			result.setYear(0);
		}
		return result;
    }

    public boolean equals(Object obj) {
    	if (obj instanceof AbstractDateTimeValue) {
    		AbstractDateTimeValue dt = (AbstractDateTimeValue) obj;
			return calendar.equals(dt.calendar);
		}
    	
    	return false;
	}
    
    public int hashCode() {
    	return calendar.hashCode();
    }
    
    //copy from org.apache.xerces.jaxp.datatype.XMLGregorianCalendarImpl
    private XMLGregorianCalendar parse(String lexicalRepresentation) {
        // compute format string for this lexical representation.
        String format = null;
        String lexRep = lexicalRepresentation;
        final int NOT_FOUND = -1;
        int lexRepLength = lexRep.length();

        // current parser needs a format string,
        // use following heuristics to figure out what xml schema date/time
        // datatype this lexical string could represent.
        if (lexRep.indexOf('T') != NOT_FOUND) {
            // found Date Time separater, must be xsd:DateTime
            format = "%Y-%M-%DT%h:%m:%s" + "%z";
        } 
        else if (lexRepLength >= 3 && lexRep.charAt(2) == ':') {
            // found ":", must be xsd:Time
            format = "%h:%m:%s" +"%z";
        } 
        else if (lexRep.startsWith("--")) {
            // check for GDay || GMonth || GMonthDay
            if (lexRepLength >= 3 && lexRep.charAt(2) == '-') {
                // GDAY
                // Fix 4971612: invalid SCCS macro substitution in data string
                format = "---%D" + "%z";
            } 
            else if (lexRepLength == 4 || (lexRepLength >= 6 && (lexRep.charAt(4) == '+' || (lexRep.charAt(4) == '-' && (lexRep.charAt(5) == '-' || lexRepLength == 10))))) {
                // GMonth
                // Fix 4971612: invalid SCCS macro substitution in data string
                format = "--%M--%Z";
                Parser p = new Parser(format, lexRep);
                try {
                	XMLGregorianCalendar c = p.parse();
                    // check for validity
                    if (!c.isValid()) {
                        throw new IllegalArgumentException(
                                DatatypeMessageFormatter.formatMessage(null,"InvalidXGCRepresentation", new Object[]{lexicalRepresentation})
                                //"\"" + lexicalRepresentation + "\" is not a valid representation of an XML Gregorian Calendar value."
                        );
                    }
                    return c;
                }
                catch(IllegalArgumentException e) {
                    format = "--%M%z";
                }
            } 
            else {
                // GMonthDay or invalid lexicalRepresentation
                format = "--%M-%D" + "%z";
            }
        } 
        else {
            // check for Date || GYear | GYearMonth
            int countSeparator = 0;

            // start at index 1 to skip potential negative sign for year.


            int timezoneOffset = lexRep.indexOf(':');
            if (timezoneOffset != NOT_FOUND) {

                // found timezone, strip it off for distinguishing
                // between Date, GYear and GYearMonth so possible
                // negative sign in timezone is not mistaken as
                // a separator.
                lexRepLength -= 6;
            }

            for (int i=1; i < lexRepLength; i++) {
                if (lexRep.charAt(i) == '-') {
                    countSeparator++;
                }
            }
            if (countSeparator == 0) {
                // GYear
                format = "%Y" + "%z";
            } 
            else if (countSeparator == 1) {
                // GYearMonth
                format = "%Y-%M" + "%z";
            } 
            else {
                // Date or invalid lexicalRepresentation
                // Fix 4971612: invalid SCCS macro substitution in data string
                format = "%Y-%M-%D" + "%z";
            }
        }
        Parser p = new Parser(format, lexRep);
        XMLGregorianCalendar c = p.parse();

        // check for validity
        if (!c.isValid()) {
            throw new IllegalArgumentException(
                    DatatypeMessageFormatter.formatMessage(null,"InvalidXGCRepresentation", new Object[]{lexicalRepresentation})
                    //"\"" + lexicalRepresentation + "\" is not a valid representation of an XML Gregorian Calendar value."
            );
        }
        return c;
    }
    
    private final class Parser {
        private final String format;
        private final String value;

        private final int flen;
        private final int vlen;

        private int fidx;
        private int vidx;
        
        private BigInteger year = null;
        private int month = DatatypeConstants.FIELD_UNDEFINED;
        private int day = DatatypeConstants.FIELD_UNDEFINED;

        private int timezone = DatatypeConstants.FIELD_UNDEFINED;

        private int hour = DatatypeConstants.FIELD_UNDEFINED;
        private int minute = DatatypeConstants.FIELD_UNDEFINED;
        private int second = DatatypeConstants.FIELD_UNDEFINED ;
        
        private BigDecimal fractionalSecond = null;

        private Parser(String format, String value) {
            this.format = format;
            this.value = value;
            this.flen = format.length();
            this.vlen = value.length();
        }
        
        /**
         * <p>Parse a formated <code>String</code> into an <code>XMLGregorianCalendar</code>.</p>
         * 
         * <p>If <code>String</code> is not formated as a legal <code>XMLGregorianCalendar</code> value,
         * an <code>IllegalArgumentException</code> is thrown.</p>
         * 
         * @throws IllegalArgumentException If <code>String</code> is not formated as a legal <code>XMLGregorianCalendar</code> value.
         */
        public XMLGregorianCalendar parse() throws IllegalArgumentException {
        	char vch;
        	while (fidx < flen) {
                char fch = format.charAt(fidx++);

                if (fch != '%') { // not a meta character
                    skip(fch);
                    continue;
                }

                // seen meta character. we don't do error check against the format
                switch (format.charAt(fidx++)) {
                    case 'Y' : // year
                        parseYear();
                        break;

                    case 'M' : // month
                    	month = parseInt(2, 2);
                        break;

                    case 'D' : // days
                    	day = parseInt(2, 2);
                        break;

                    case 'h' : // hours
                    	hour = parseInt(2, 2);
                        break;

                    case 'm' : // minutes
                    	minute = parseInt(2, 2);
                        break;

                    case 's' : // parse seconds.
                    	second = parseInt(2, 2);

                        if (peek() == '.') {
                        	fractionalSecond = parseBigDecimal();
                        }
                        break;

                    case 'z' : // time zone. missing, 'Z', or [+-]nn:nn
                        vch = peek();
                        if (vch == 'Z') {
                            vidx++;
                            timezone = 0;
                        } 
                        else if (vch == '+' || vch == '-') {
                            vidx++;
                            int h = parseInt(2, 2);
                            skip(':');
                            int m = parseInt(2, 2);

                            if (m >= 60 || m < 0)
                                throw new IllegalArgumentException(
                                        DatatypeMessageFormatter.formatMessage(null, "InvalidFieldValue", new Object[]{ new Integer(m), "timezone minutes"})
                                );
                            
                            timezone = (h * 60 + m) * (vch == '+' ? 1 : -1);
                        }
                        break;

                    case 'Z' : // time zone. 'Z', or [+-]nn:nn
                        vch = peek();
                        if (vch == 'Z') {
                            vidx++;
                            timezone = 0;
                        } 
                        else if (vch == '+' || vch == '-') {
                            vidx++;
                            int h = parseInt(2, 2);
                            skip(':');
                            int m = parseInt(2, 2);

                            if (m >= 60 || m < 0)
                                throw new IllegalArgumentException(
                                        DatatypeMessageFormatter.formatMessage(null, "InvalidFieldValue", new Object[]{ new Integer(m), "timezone minutes"})
                                );
                            
                            timezone = (h * 60 + m) * (vch == '+' ? 1 : -1);
                        } else {
                            throw new IllegalArgumentException(
                                    DatatypeMessageFormatter.formatMessage(null, "InvalidFieldValue", new Object[]{ "do not defined", "timezone"})
                            );
                        }
                        break;

                    default :
                        // illegal meta character. impossible.
                        throw new InternalError();
                }
            }

            if (vidx != vlen) {
                // some tokens are left in the input
                throw new IllegalArgumentException(value); //,vidx);
            }
            
            if (hour == 24 && minute == 0 && second == 0) hour = 0;
            
            return TimeUtils.getInstance().getFactory()
        		.newXMLGregorianCalendar(year, month, day, hour, minute, second, fractionalSecond, timezone);
        }
        
        private char peek() throws IllegalArgumentException {
            if (vidx == vlen) {
                return (char) -1;<commit_msg>[ignore] round hour for time value only<commit_after>/*
 * eXist Open Source Native XML Database
 * Copyright (C) 2001-2007 The eXist Project
 * http://exist-db.org
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *  
 *  $Id$
 */
package org.exist.xquery.value;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.Collator;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;

import org.apache.xerces.util.DatatypeMessageFormatter;
import org.exist.xquery.Constants;
import org.exist.xquery.ErrorCodes;
import org.exist.xquery.XPathException;

/**
 * @author wolf
 * @author <a href="mailto:<EMAIL>"><NAME></a>
 * @author ljo
 */
public abstract class AbstractDateTimeValue extends ComputableValue {
	
	//Provisionally public
	public final XMLGregorianCalendar calendar;
	private XMLGregorianCalendar implicitCalendar, canonicalCalendar, trimmedCalendar;
	
	protected static Pattern negativeDateStart = Pattern.compile("^\\d\\d?-(\\d+)-(.*)"); 

	public final static int YEAR = 0;
	public final static int MONTH = 1;
	public final static int DAY = 2;
	public final static int HOUR = 3;
	public final static int MINUTE = 4;
	public final static int SECOND = 5;
	public final static int MILLISECOND = 6;

	/**
	 * Create a new date time value based on the given calendar.  The calendar is
	 * <em>not</em> cloned, so it is the subclass's responsibility to make sure there are
	 * no external references to it that would allow for mutation.
	 *
	 * @param calendar the calendar to wrap into an XPath value
	 */
	protected AbstractDateTimeValue(XMLGregorianCalendar calendar) {
		this.calendar = calendar;
	}
	
	protected AbstractDateTimeValue(String lexicalValue) throws XPathException {
        lexicalValue = StringValue.trimWhitespace(lexicalValue);

        //lexicalValue = normalizeDate(lexicalValue);
        //lexicalValue = normalizeTime(getType(), lexicalValue);
		try {
			calendar = parse(lexicalValue);
		} catch (IllegalArgumentException e) {
			throw new XPathException(ErrorCodes.FORG0001, "illegal lexical form for date-time-like value '" + lexicalValue + "' " + e.getMessage(), e);
		}
	}
	
	/**
	 * Return a calendar with the timezone field set, to be used for order comparison.
	 * If the original calendar did not specify a timezone, set the local timezone (unadjusted
	 * for daylight savings).  The returned calendars will be totally ordered between themselves.
	 * We also set any missing fields to ensure that normalization doesn't discard important data!
	 * (This is probably a bug in the JAXP implementation, but the workaround doesn't hurt us,
	 * so it's faster to just fix it here.)
	 *
	 * @return the calendar represented by this object, with the timezone field filled in with an implicit value if necessary
	 */
	protected XMLGregorianCalendar getImplicitCalendar() {
		if (implicitCalendar == null) {
			implicitCalendar = (XMLGregorianCalendar) calendar.clone();
			if (calendar.getTimezone() == DatatypeConstants.FIELD_UNDEFINED) {
				implicitCalendar.setTimezone(TimeUtils.getInstance().getLocalTimezoneOffsetMinutes());
			}
			// fill in fields from default reference; don't have to worry about weird combinations of fields being set, since we control that on creation
			switch(getType()) {
				case Type.DATE: implicitCalendar.setTime(0,0,0); break;
				case Type.TIME: implicitCalendar.setYear(1972); implicitCalendar.setMonth(12); implicitCalendar.setDay(31); break;
				default:
			}
			implicitCalendar = implicitCalendar.normalize();	// the comparison routines will normalize it anyway, just do it once here
		}
		return implicitCalendar;
	}
	
	// TODO: method not currently used, apparently the XPath spec never needs to canonicalize
	// date/times after all (see section 17.1.2 on casting)
	protected XMLGregorianCalendar getCanonicalCalendar() {
		if (canonicalCalendar == null) {
			canonicalCalendar = getTrimmedCalendar().normalize();
		}
		return canonicalCalendar;
	}
	
	protected XMLGregorianCalendar getTrimmedCalendar() {
		if (trimmedCalendar == null) {
			trimmedCalendar = cloneXMLGregorianCalendar(calendar);
			BigDecimal fract = trimmedCalendar.getFractionalSecond();
			if (fract != null) {
				// TODO: replace following algorithm in JDK 1.5 with fract.stripTrailingZeros();
				String s = fract.toString();
				int i = s.length();
				while (i > 0 && s.charAt(i-1) == '0') i--;
				if (i == 0) trimmedCalendar.setFractionalSecond(null);
				else if (i != s.length()) trimmedCalendar.setFractionalSecond(new BigDecimal(s.substring(0, i)));
			}
		}
		return trimmedCalendar;
	}
	
	protected abstract AbstractDateTimeValue createSameKind(XMLGregorianCalendar cal) throws XPathException;
	
	protected long getTimeInMillis() {
		// use getImplicitCalendar() rather than relying on toGregorianCalendar timezone defaulting
		// to maintain consistency
		return getImplicitCalendar().toGregorianCalendar().getTimeInMillis();
	}
	
	protected abstract QName getXMLSchemaType();
	
	public String getStringValue() throws XPathException {
		String r = getTrimmedCalendar().toXMLFormat();
		// hacked to match the format mandated in XPath 2 17.1.2, which is different from the XML Schema canonical format
		//if (r.charAt(r.length()-1) == 'Z') r = r.substring(0, r.length()-1) + "+00:00";
		
		//Let's try these lexical transformations...		
		boolean startsWithDashDash = r.startsWith("--");
		r = r.replaceAll("--", "");
		if (startsWithDashDash)
			r = "--" + r;
		
		Matcher m = negativeDateStart.matcher(r);
		if (m.matches()) {
			int year = Integer.valueOf(m.group(1)).intValue();
			DecimalFormat df = new DecimalFormat("0000");
			r = "-" + df.format(year) + "-" + m.group(2);
		}
		
		return r;
	}

	public boolean effectiveBooleanValue() throws XPathException {
		throw new XPathException(ErrorCodes.FORG0006, "effective boolean value invalid operand type: " + Type.getTypeName(getType()));
	}
	
	public abstract AtomicValue convertTo(int requiredType) throws XPathException;

	public int getPart(int part) {
		switch (part) {
			case YEAR: return calendar.getYear(); 
			case MONTH: return calendar.getMonth();
			case DAY: return calendar.getDay();
			case HOUR: return calendar.getHour();
			case MINUTE: return calendar.getMinute();
			case SECOND: return calendar.getSecond();
			case MILLISECOND: int mSec=calendar.getMillisecond();
                                          if(mSec == DatatypeConstants.FIELD_UNDEFINED)
                                              return 0;
                                          else
                                              return calendar.getMillisecond();
			default: throw new IllegalArgumentException("Invalid argument to method getPart");
		}
	}
	
	private static final Duration tzLowerBound = TimeUtils.getInstance().newDurationDayTime("-PT14H");
	private static final Duration tzUpperBound = tzLowerBound.negate();
	protected void validateTimezone(DayTimeDurationValue offset) throws XPathException {
		Duration tz = offset.duration;
		Number secs = tz.getField(DatatypeConstants.SECONDS);
		if (secs != null && ((BigDecimal) secs).compareTo(BigDecimal.valueOf(0)) != 0)
			throw new XPathException(ErrorCodes.FODT0003, "duration " + offset + " has fractional minutes so cannot be used as a timezone offset");
		if (! (
				tz.equals(tzLowerBound) ||
				tz.equals(tzUpperBound) ||
				(tz.isLongerThan(tzLowerBound) && tz.isShorterThan(tzUpperBound))
			))
			throw new XPathException(ErrorCodes.FODT0003, "duration " + offset + " outside valid timezone offset range");
	}

	public AbstractDateTimeValue adjustedToTimezone(DayTimeDurationValue offset) throws XPathException {
		if (offset == null) offset = new DayTimeDurationValue(TimeUtils.getInstance().getLocalTimezoneOffsetMillis());
		validateTimezone(offset);
		XMLGregorianCalendar xgc = (XMLGregorianCalendar) calendar.clone();
		if (xgc.getTimezone() != DatatypeConstants.FIELD_UNDEFINED) {
			if (getType() == Type.DATE) xgc.setTime(0,0,0);	// set the fields so we don't lose precision when shifting timezones
			xgc = xgc.normalize();
			xgc.add(offset.duration);
		}
		try {
			xgc.setTimezone((int) (offset.getValue()/60));
		} catch (IllegalArgumentException e) {
			throw new XPathException(ErrorCodes.FORG0001, "illegal timezone offset " + offset, e);
		}
		return createSameKind(xgc);
	}
	
	public AbstractDateTimeValue withoutTimezone() throws XPathException {
		XMLGregorianCalendar xgc = (XMLGregorianCalendar) calendar.clone();
		xgc.setTimezone(DatatypeConstants.FIELD_UNDEFINED);
		return createSameKind(xgc);
	}
	
	public Sequence getTimezone() throws XPathException {
		int tz = calendar.getTimezone();
		if (tz == DatatypeConstants.FIELD_UNDEFINED) return Sequence.EMPTY_SEQUENCE;
		return new DayTimeDurationValue(tz * 60000L);
	}
	
	public boolean compareTo(Collator collator, int operator, AtomicValue other) throws XPathException {
		int cmp = compareTo(collator, other);
		switch (operator) {
			case Constants.EQ: return cmp == 0;
			case Constants.NEQ: return cmp != 0;
			case Constants.LT: return cmp < 0;
			case Constants.LTEQ: return cmp <= 0;
			case Constants.GT: return cmp > 0;
			case Constants.GTEQ: return cmp >= 0;
			default :
				throw new XPathException("Unknown operator type in comparison");
		}
	}

	public int compareTo(Collator collator, AtomicValue other) throws XPathException {
		if (other.getType() == getType()) {
			// filling in missing timezones with local timezone, should be total order as per XPath 2.0 10.4
			int r =  getImplicitCalendar().compare(((AbstractDateTimeValue) other).getImplicitCalendar());
			if (r == DatatypeConstants.INDETERMINATE) throw new RuntimeException("indeterminate order between " + this + " and " + other);
			return r;
		}
		throw new XPathException(
			"Type error: cannot compare " + Type.getTypeName(getType()) + " to "
				+ Type.getTypeName(other.getType()));
	}

	public AtomicValue max(Collator collator, AtomicValue other) throws XPathException {
		AbstractDateTimeValue otherDate = other.getType() == getType() ? (AbstractDateTimeValue) other : (AbstractDateTimeValue) other.convertTo(getType());
		return getImplicitCalendar().compare(otherDate.getImplicitCalendar()) > 0 ? this : other;
	}

	public AtomicValue min(Collator collator, AtomicValue other) throws XPathException {
		AbstractDateTimeValue otherDate = other.getType() == getType() ? (AbstractDateTimeValue) other : (AbstractDateTimeValue) other.convertTo(getType());
		return getImplicitCalendar().compare(otherDate.getImplicitCalendar()) < 0 ? this : other;
	}

	// override for xs:time
	public ComputableValue plus(ComputableValue other) throws XPathException {
		switch(other.getType()) {
			case Type.YEAR_MONTH_DURATION:
			case Type.DAY_TIME_DURATION:
				return other.plus(this);
			default:
				throw new XPathException(
						"Operand to plus should be of type xdt:dayTimeDuration or xdt:yearMonthDuration; got: "
							+ Type.getTypeName(other.getType()));
		}
	}

	public ComputableValue mult(ComputableValue other) throws XPathException {
		throw new XPathException("multiplication is not supported for type " + Type.getTypeName(getType()));
	}

	public ComputableValue div(ComputableValue other) throws XPathException {
		throw new XPathException("division is not supported for type " + Type.getTypeName(getType()));
	}

	public int conversionPreference(Class<?> javaClass) {
		if (javaClass.isAssignableFrom(DateValue.class)) return 0;
		if (javaClass.isAssignableFrom(XMLGregorianCalendar.class)) return 1;
		if (javaClass.isAssignableFrom(GregorianCalendar.class)) return 2;
		if (javaClass == Date.class) return 3;
		return Integer.MAX_VALUE;
	}

	public Object toJavaObject(Class<?> target) throws XPathException {
		if (target == Object.class || target.isAssignableFrom(DateValue.class))
			return this;
		else if (target.isAssignableFrom(XMLGregorianCalendar.class))
			return calendar.clone();
		else if (target.isAssignableFrom(GregorianCalendar.class))
			return calendar.toGregorianCalendar();
		else if (target == Date.class)
			return calendar.toGregorianCalendar().getTime();

		throw new XPathException("cannot convert value of type " + Type.getTypeName(getType()) + " to Java object of type " + target.getName());
	}
    
    /* (non-Javadoc)
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    public int compareTo(Object o)
    {
        if (o instanceof AbstractDateTimeValue) {
			AbstractDateTimeValue dt = (AbstractDateTimeValue) o;
    		return calendar.compare(dt.calendar);
		}

        final AtomicValue other = (AtomicValue)o;
        if(Type.subTypeOf(other.getType(), Type.DATE_TIME))
        	try {
        		//TODO : find something that will consume less resources
        		return calendar.compare(TimeUtils.getInstance().newXMLGregorianCalendar(other.getStringValue()));
        	} catch (XPathException e) {
        		System.out.println("Failed to get string value of '" + other + "'");
        		//Why not ?
        		return Constants.SUPERIOR;
        	}
        else
            return getType() > other.getType() ? Constants.SUPERIOR : Constants.INFERIOR;
    }	

    /**
     * Utility method that is able to clone a calendar whose year is 0
     * (whatever a year 0 means). 
     * It looks like the JDK is unable to do that.
     * @param calendar The Calendar to clone
     * @return the cloned Calendar
     */
    public static XMLGregorianCalendar cloneXMLGregorianCalendar(XMLGregorianCalendar calendar) {
    	boolean hacked = false;
		if (calendar.getYear() == 0) {
			calendar.setYear(1);
			hacked = true;
		}
		XMLGregorianCalendar result = (XMLGregorianCalendar)calendar.clone();
		if (hacked) {
			//reset everything
			calendar.setYear(0);
			//-1 could also be considered
			result.setYear(0);
		}
		return result;
    }

    public boolean equals(Object obj) {
    	if (obj instanceof AbstractDateTimeValue) {
    		AbstractDateTimeValue dt = (AbstractDateTimeValue) obj;
			return calendar.equals(dt.calendar);
		}
    	
    	return false;
	}
    
    public int hashCode() {
    	return calendar.hashCode();
    }
    
    //copy from org.apache.xerces.jaxp.datatype.XMLGregorianCalendarImpl
    private XMLGregorianCalendar parse(String lexicalRepresentation) {
        // compute format string for this lexical representation.
        String format = null;
        String lexRep = lexicalRepresentation;
        final int NOT_FOUND = -1;
        int lexRepLength = lexRep.length();

        // current parser needs a format string,
        // use following heuristics to figure out what xml schema date/time
        // datatype this lexical string could represent.
        if (lexRep.indexOf('T') != NOT_FOUND) {
            // found Date Time separater, must be xsd:DateTime
            format = "%Y-%M-%DT%h:%m:%s" + "%z";
        } 
        else if (lexRepLength >= 3 && lexRep.charAt(2) == ':') {
            // found ":", must be xsd:Time
            format = "%h:%m:%s" +"%z";
        } 
        else if (lexRep.startsWith("--")) {
            // check for GDay || GMonth || GMonthDay
            if (lexRepLength >= 3 && lexRep.charAt(2) == '-') {
                // GDAY
                // Fix 4971612: invalid SCCS macro substitution in data string
                format = "---%D" + "%z";
            } 
            else if (lexRepLength == 4 || (lexRepLength >= 6 && (lexRep.charAt(4) == '+' || (lexRep.charAt(4) == '-' && (lexRep.charAt(5) == '-' || lexRepLength == 10))))) {
                // GMonth
                // Fix 4971612: invalid SCCS macro substitution in data string
                format = "--%M--%Z";
                Parser p = new Parser(format, lexRep);
                try {
                	XMLGregorianCalendar c = p.parse();
                    // check for validity
                    if (!c.isValid()) {
                        throw new IllegalArgumentException(
                                DatatypeMessageFormatter.formatMessage(null,"InvalidXGCRepresentation", new Object[]{lexicalRepresentation})
                                //"\"" + lexicalRepresentation + "\" is not a valid representation of an XML Gregorian Calendar value."
                        );
                    }
                    return c;
                }
                catch(IllegalArgumentException e) {
                    format = "--%M%z";
                }
            } 
            else {
                // GMonthDay or invalid lexicalRepresentation
                format = "--%M-%D" + "%z";
            }
        } 
        else {
            // check for Date || GYear | GYearMonth
            int countSeparator = 0;

            // start at index 1 to skip potential negative sign for year.


            int timezoneOffset = lexRep.indexOf(':');
            if (timezoneOffset != NOT_FOUND) {

                // found timezone, strip it off for distinguishing
                // between Date, GYear and GYearMonth so possible
                // negative sign in timezone is not mistaken as
                // a separator.
                lexRepLength -= 6;
            }

            for (int i=1; i < lexRepLength; i++) {
                if (lexRep.charAt(i) == '-') {
                    countSeparator++;
                }
            }
            if (countSeparator == 0) {
                // GYear
                format = "%Y" + "%z";
            } 
            else if (countSeparator == 1) {
                // GYearMonth
                format = "%Y-%M" + "%z";
            } 
            else {
                // Date or invalid lexicalRepresentation
                // Fix 4971612: invalid SCCS macro substitution in data string
                format = "%Y-%M-%D" + "%z";
            }
        }
        Parser p = new Parser(format, lexRep);
        XMLGregorianCalendar c = p.parse();

        // check for validity
        if (!c.isValid()) {
            throw new IllegalArgumentException(
                    DatatypeMessageFormatter.formatMessage(null,"InvalidXGCRepresentation", new Object[]{lexicalRepresentation})
                    //"\"" + lexicalRepresentation + "\" is not a valid representation of an XML Gregorian Calendar value."
            );
        }
        return c;
    }
    
    private final class Parser {
        private final String format;
        private final String value;

        private final int flen;
        private final int vlen;

        private int fidx;
        private int vidx;
        
        private BigInteger year = null;
        private int month = DatatypeConstants.FIELD_UNDEFINED;
        private int day = DatatypeConstants.FIELD_UNDEFINED;

        private int timezone = DatatypeConstants.FIELD_UNDEFINED;

        private int hour = DatatypeConstants.FIELD_UNDEFINED;
        private int minute = DatatypeConstants.FIELD_UNDEFINED;
        private int second = DatatypeConstants.FIELD_UNDEFINED ;
        
        private BigDecimal fractionalSecond = null;

        private Parser(String format, String value) {
            this.format = format;
            this.value = value;
            this.flen = format.length();
            this.vlen = value.length();
        }
        
        /**
         * <p>Parse a formated <code>String</code> into an <code>XMLGregorianCalendar</code>.</p>
         * 
         * <p>If <code>String</code> is not formated as a legal <code>XMLGregorianCalendar</code> value,
         * an <code>IllegalArgumentException</code> is thrown.</p>
         * 
         * @throws IllegalArgumentException If <code>String</code> is not formated as a legal <code>XMLGregorianCalendar</code> value.
         */
        public XMLGregorianCalendar parse() throws IllegalArgumentException {
        	char vch;
        	while (fidx < flen) {
                char fch = format.charAt(fidx++);

                if (fch != '%') { // not a meta character
                    skip(fch);
                    continue;
                }

                // seen meta character. we don't do error check against the format
                switch (format.charAt(fidx++)) {
                    case 'Y' : // year
                        parseYear();
                        break;

                    case 'M' : // month
                    	month = parseInt(2, 2);
                        break;

                    case 'D' : // days
                    	day = parseInt(2, 2);
                        break;

                    case 'h' : // hours
                    	hour = parseInt(2, 2);
                        break;

                    case 'm' : // minutes
                    	minute = parseInt(2, 2);
                        break;

                    case 's' : // parse seconds.
                    	second = parseInt(2, 2);

                        if (peek() == '.') {
                        	fractionalSecond = parseBigDecimal();
                        }
                        break;

                    case 'z' : // time zone. missing, 'Z', or [+-]nn:nn
                        vch = peek();
                        if (vch == 'Z') {
                            vidx++;
                            timezone = 0;
                        } 
                        else if (vch == '+' || vch == '-') {
                            vidx++;
                            int h = parseInt(2, 2);
                            skip(':');
                            int m = parseInt(2, 2);

                            if (m >= 60 || m < 0)
                                throw new IllegalArgumentException(
                                        DatatypeMessageFormatter.formatMessage(null, "InvalidFieldValue", new Object[]{ new Integer(m), "timezone minutes"})
                                );
                            
                            timezone = (h * 60 + m) * (vch == '+' ? 1 : -1);
                        }
                        break;

                    case 'Z' : // time zone. 'Z', or [+-]nn:nn
                        vch = peek();
                        if (vch == 'Z') {
                            vidx++;
                            timezone = 0;
                        } 
                        else if (vch == '+' || vch == '-') {
                            vidx++;
                            int h = parseInt(2, 2);
                            skip(':');
                            int m = parseInt(2, 2);

                            if (m >= 60 || m < 0)
                                throw new IllegalArgumentException(
                                        DatatypeMessageFormatter.formatMessage(null, "InvalidFieldValue", new Object[]{ new Integer(m), "timezone minutes"})
                                );
                            
                            timezone = (h * 60 + m) * (vch == '+' ? 1 : -1);
                        } else {
                            throw new IllegalArgumentException(
                                    DatatypeMessageFormatter.formatMessage(null, "InvalidFieldValue", new Object[]{ "do not defined", "timezone"})
                            );
                        }
                        break;

                    default :
                        // illegal meta character. impossible.
                        throw new InternalError();
                }
            }

            if (vidx != vlen) {
                // some tokens are left in the input
                throw new IllegalArgumentException(value); //,vidx);
            }
            
           	if (hour == 24 && minute == 0 && second == 0) {
                if (getType() == Type.TIME)
                	hour = 0;
           	}
            
            return TimeUtils.getInstance().getFactory()
        		.newXMLGregorianCalendar(year, month, day, hour, minute, second, fractionalSecond, timezone);
        }
        
        private char peek() throws IllegalArgumentException {
            if (vidx == vlen) {
                return (char) -1;
{'fraction_non_alphanumeric': 0.10213202874916953, 'fraction_numerical': 0.008254313382053916, 'mean_word_length': 2.2168900977915937, 'pattern_counts': {'":': 2, '<': 69, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 10}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 30, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1558841', 'n_tokens_mistral': 14560, 'n_tokens_neox': 13041, 'n_words': 3878}