public int parseInto(ReadWritableInstant instant, String text, int position) {
    DateTimeParser parser = requireParser();
    if (instant == null) {
        throw new IllegalArgumentException("Instant must not be null");
    }

    long instantMillis = instant.getMillis();
    Chronology chrono = instant.getChronology();
    long instantLocal = instantMillis + chrono.getZone().getOffset(instantMillis);
    chrono = selectChronology(chrono);

    DateTimeParserBucket bucket = new DateTimeParserBucket(
        instantLocal, chrono, iLocale, iPivotYear, iDefaultYear);
    int newPos = parser.parseInto(bucket, text, position);
    instant.setMillis(bucket.computeMillis(false, text));
    if (iOffsetParsed && bucket.getOffsetInteger() != null) {
        int parsedOffset = bucket.getOffsetInteger();
        DateTimeZone parsedZone = DateTimeZone.forOffsetMillis(parsedOffset);
        chrono = chrono.withZone(parsedZone);
    } else if (bucket.getZone() != null) {
        chrono = chrono.withZone(bucket.getZone());
    }
    instant.setChronology(chrono);
    if (iZone != null) {
        instant.setZone(iZone);
    }
    return newPos;
}

public int parseInto(ReadWritableInstant instant, String text, int position) {
    DateTimeParser parser = requireParser();
    if (instant == null) {
        throw new IllegalArgumentException("Instant must not be null");
    }

    long instantMillis = instant.getMillis();
    Chronology chrono = instant.getChronology();
    long instantLocal = instantMillis + chrono.getZone().getOffset(instantMillis);
    chrono = selectChronology(chrono);

    DateTimeParserBucket bucket = new DateTimeParserBucket(
        instantLocal, chrono, iLocale, iPivotYear, chrono.year().get(instantLocal));
    int newPos = parser.parseInto(bucket, text, position);
    instant.setMillis(bucket.computeMillis(false, text));
    if (iOffsetParsed && bucket.getOffsetInteger() != null) {
        int parsedOffset = bucket.getOffsetInteger();
        DateTimeZone parsedZone = DateTimeZone.forOffsetMillis(parsedOffset);
        chrono = chrono.withZone(parsedZone);
    } else if (bucket.getZone() != null) {
        chrono = chrono.withZone(bucket.getZone());
    }
    instant.setChronology(chrono);
    if (iZone != null) {
        instant.setZone(iZone);
    }
    return newPos;
}

#148 DateTimeFormatter.parseInto broken when no year in format

In Joda Time 2.0, the default year was set to 2000 so that Feb 29 could be parsed correctly. However, parseInto now overwrites the given instant's year with 2000 (or whatever iDefaultYear is set to). The correct behavior would seem to be to use the given instant's year instead of iDefaultYear.
This does mean that Feb 29 might not be parseable if the instant's year is not a leap year, but in this case the caller asked for that in a sense.


public long adjustOffset(long instant, boolean earlierOrLater) {
    // a bit messy, but will work in all non-pathological cases

    // evaluate 3 hours before and after to work out if anything is happening
    long instantBefore = convertUTCToLocal(instant - 3 * DateTimeConstants.MILLIS_PER_HOUR);
    long instantAfter = convertUTCToLocal(instant + 3 * DateTimeConstants.MILLIS_PER_HOUR);
    if (instantBefore == instantAfter) {
        return instant;  // not an overlap (less than is a gap, equal is normal case)
    }

    // work out range of instants that have duplicate local times
    long local = convertUTCToLocal(instant);
    return convertLocalToUTC(local, false, earlierOrLater ? instantAfter : instantBefore);

    // calculate result
      // currently in later offset
      // currently in earlier offset
}

public long adjustOffset(long instant, boolean earlierOrLater) {
    // a bit messy, but will work in all non-pathological cases

    // evaluate 3 hours before and after to work out if anything is happening
    long instantBefore = instant - 3 * DateTimeConstants.MILLIS_PER_HOUR;
    long instantAfter = instant + 3 * DateTimeConstants.MILLIS_PER_HOUR;
    long offsetBefore = getOffset(instantBefore);
    long offsetAfter = getOffset(instantAfter);
    if (offsetBefore <= offsetAfter) {
        return instant;  // not an overlap (less than is a gap, equal is normal case)
    }

    // work out range of instants that have duplicate local times
    long diff = offsetBefore - offsetAfter;
    long transition = nextTransition(instantBefore);
    long overlapStart = transition - diff;
    long overlapEnd = transition + diff;
    if (instant < overlapStart || instant >= overlapEnd) {
      return instant;  // not an overlap
    }

    // calculate result
    long afterStart = instant - overlapStart;
    if (afterStart >= diff) {
      // currently in later offset
      return earlierOrLater ? instant : instant - diff;
    } else {
      // currently in earlier offset
      return earlierOrLater ? instant + diff : instant;
    }
}

#141 Bug on withLaterOffsetAtOverlap method

The method withLaterOffsetAtOverlap created to workaround the issue 3192457 seems to not be working at all.
I won´t write many info about the problem to solve because the issue 3192457 have this info indeed.
But If something is unclear I can answer on the comments.
Problem demonstration:
TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
DateTimeZone.setDefault( DateTimeZone.forID("America/Sao_Paulo") );
    DateTime dtch;
    {
        dtch = new DateTime(2012,2,25,5,5,5,5).millisOfDay().withMaximumValue();
        System.out.println( dtch ); // prints: 2012-02-25T23:59:59.999-02:00 //Were are at the first 23:** of the day.
        //At this point dtch have the -03:00 offset
    }
    {
        dtch = dtch.plus(60001);
        System.out.println( dtch ); // prints: 2012-02-25T23:01:00.000-03:00 //Were are at the first minute of the second 23:** of the day. Ok its correct
        //At this point dtch have the -03:00 offset
    }
    {
        dtch = dtch.withEarlierOffsetAtOverlap();
        System.out.println( dtch ); // prints: 2012-02-25T23:01:00.000-02:00 //Were are at the first minute of the first 23:** of the day. Ok its correct
        //At this point dtch have the -02:00 offset ( because we called withEarlierOffsetAtOverlap() ) // This method is working perfectly
    }
    {
        dtch = dtch.withLaterOffsetAtOverlap();
        System.out.println( dtch ); // prints: 2012-02-25T23:01:00.000-02:00 //Were are at the first minute of the first 23:** of the day.
        // Here is the problem we should have a -03:00 offset here since we called withLaterOffsetAtOverlap() expecting to change to the second 23:** of the day
    }

On the last two brackets we can see that withLaterOffsetAtOverlap is not undoing withEarlierOffsetAtOverlap as it should ( and not even working at all )


public long getDateTimeMillis(int year, int monthOfYear, int dayOfMonth,
                              int hourOfDay, int minuteOfHour,
                              int secondOfMinute, int millisOfSecond)
    throws IllegalArgumentException
{
    Chronology base;
    if ((base = getBase()) != null) {
        return base.getDateTimeMillis
            (year, monthOfYear, dayOfMonth,
             hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
    }

    // Assume date is Gregorian.
    long instant;
        instant = iGregorianChronology.getDateTimeMillis
            (year, monthOfYear, dayOfMonth,
             hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
    if (instant < iCutoverMillis) {
        // Maybe it's Julian.
        instant = iJulianChronology.getDateTimeMillis
            (year, monthOfYear, dayOfMonth,
             hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
        if (instant >= iCutoverMillis) {
            // Okay, it's in the illegal cutover gap.
            throw new IllegalArgumentException("Specified date does not exist");
        }
    }
    return instant;
}

public long getDateTimeMillis(int year, int monthOfYear, int dayOfMonth,
                              int hourOfDay, int minuteOfHour,
                              int secondOfMinute, int millisOfSecond)
    throws IllegalArgumentException
{
    Chronology base;
    if ((base = getBase()) != null) {
        return base.getDateTimeMillis
            (year, monthOfYear, dayOfMonth,
             hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
    }

    // Assume date is Gregorian.
    long instant;
    try {
        instant = iGregorianChronology.getDateTimeMillis
            (year, monthOfYear, dayOfMonth,
             hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
    } catch (IllegalFieldValueException ex) {
        if (monthOfYear != 2 || dayOfMonth != 29) {
            throw ex;
        }
        instant = iGregorianChronology.getDateTimeMillis
            (year, monthOfYear, 28,
             hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
        if (instant >= iCutoverMillis) {
            throw ex;
        }
    }
    if (instant < iCutoverMillis) {
        // Maybe it's Julian.
        instant = iJulianChronology.getDateTimeMillis
            (year, monthOfYear, dayOfMonth,
             hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond);
        if (instant >= iCutoverMillis) {
            // Okay, it's in the illegal cutover gap.
            throw new IllegalArgumentException("Specified date does not exist");
        }
    }
    return instant;
}

#130 GJChronology rejects valid Julian dates

Example:
DateTime jdt = new DateTime(1500, 2, 29, 0, 0, 0, 0, JulianChronology.getInstanceUTC()); // Valid.
DateTime gjdt = new DateTime(1500, 2, 29, 0, 0, 0, 0, GJChronology.getInstanceUTC()); // Invalid.
The 2nd statement fails with "org.joda.time.IllegalFieldValueException: Value 29 for dayOfMonth must be in the range [1,28]".
Given that I left the cutover date at the default (October 15, 1582), isn't 1500/02/29 a valid date in the GJChronology?


public int getOffsetFromLocal(long instantLocal) {
    // get the offset at instantLocal (first estimate)
    final int offsetLocal = getOffset(instantLocal);
    // adjust instantLocal using the estimate and recalc the offset
    final long instantAdjusted = instantLocal - offsetLocal;
    final int offsetAdjusted = getOffset(instantAdjusted);
    // if the offsets differ, we must be near a DST boundary
    if (offsetLocal != offsetAdjusted) {
        // we need to ensure that time is always after the DST gap
        // this happens naturally for positive offsets, but not for negative
        if ((offsetLocal - offsetAdjusted) < 0) {
            // if we just return offsetAdjusted then the time is pushed
            // back before the transition, whereas it should be
            // on or after the transition
            long nextLocal = nextTransition(instantAdjusted);
            long nextAdjusted = nextTransition(instantLocal - offsetAdjusted);
            if (nextLocal != nextAdjusted) {
                return offsetLocal;
            }
        }
    } else if (offsetLocal > 0) {
        long prev = previousTransition(instantAdjusted);
        if (prev < instantAdjusted) {
            int offsetPrev = getOffset(prev);
            int diff = offsetPrev - offsetLocal;
            if (instantAdjusted - prev <= diff) {
                return offsetPrev;
            }
        }
    }
    return offsetAdjusted;
}

public int getOffsetFromLocal(long instantLocal) {
    // get the offset at instantLocal (first estimate)
    final int offsetLocal = getOffset(instantLocal);
    // adjust instantLocal using the estimate and recalc the offset
    final long instantAdjusted = instantLocal - offsetLocal;
    final int offsetAdjusted = getOffset(instantAdjusted);
    // if the offsets differ, we must be near a DST boundary
    if (offsetLocal != offsetAdjusted) {
        // we need to ensure that time is always after the DST gap
        // this happens naturally for positive offsets, but not for negative
        if ((offsetLocal - offsetAdjusted) < 0) {
            // if we just return offsetAdjusted then the time is pushed
            // back before the transition, whereas it should be
            // on or after the transition
            long nextLocal = nextTransition(instantAdjusted);
            long nextAdjusted = nextTransition(instantLocal - offsetAdjusted);
            if (nextLocal != nextAdjusted) {
                return offsetLocal;
            }
        }
    } else if (offsetLocal >= 0) {
        long prev = previousTransition(instantAdjusted);
        if (prev < instantAdjusted) {
            int offsetPrev = getOffset(prev);
            int diff = offsetPrev - offsetLocal;
            if (instantAdjusted - prev <= diff) {
                return offsetPrev;
            }
        }
    }
    return offsetAdjusted;
}

#124 Inconsistent interpretation of ambiguous time during DST

The inconsistency appears for timezone Europe/London.
Consider the following code
…
DateTime britishDate = new DateTime(2011, 10, 30, 1, 59, 0, 0, DateTimeZone.forID("Europe/London"));
DateTime norwDate = new DateTime(2011, 10, 30, 2, 59, 0, 0, DateTimeZone.forID("Europe/Oslo"));
DateTime finnishDate = new DateTime(2011, 10, 30, 3, 59, 0, 0, DateTimeZone.forID("Europe/Helsinki"));
    System.out.println(britishDate);
    System.out.println(norwDate);
    System.out.println(finnishDate);

…
These three DateTime objects should all represent the same moment in time even if they are ambiguous. And using jodatime 1.6.2 this is the case. The code produces the following output:
2011-10-30T01:59:00.000Z
2011-10-30T02:59:00.000+01:00
2011-10-30T03:59:00.000+02:00
Using jodatime 2.0 however, the output is:
2011-10-30T01:59:00.000Z
2011-10-30T02:59:00.000+02:00
2011-10-30T03:59:00.000+03:00
which IMO is wrong for Europe/London. Correct output should have been
2011-10-30T01:59:00.000+01:00
The release notes for 2.0 states that:
"Now, it always returns the earlier instant (summer time) during an overlap. …"


public int parseInto(DateTimeParserBucket bucket, String text, int position) {
    String str = text.substring(position);
    for (String id : ALL_IDS) {
        if (str.startsWith(id)) {
            bucket.setZone(DateTimeZone.forID(id));
            return position + id.length();
        }
    }
    return ~position;
}

public int parseInto(DateTimeParserBucket bucket, String text, int position) {
    String str = text.substring(position);
    String best = null;
    for (String id : ALL_IDS) {
        if (str.startsWith(id)) {
        	if (best == null || id.length() > best.length()) {
        		best = id;
        	}
        }
    }
    if (best != null) {
        bucket.setZone(DateTimeZone.forID(best));
        return position + best.length();
    }
    return ~position;
}

#126 Errors creating/parsing dates with specific time zones.

Consider the following test code using Joda 2.0
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.Set;
public class JodaDateTimeZoneTester {
private static DateTimeFormatter formatter = DateTimeFormat.forPattern("MM/dd/yyyy HH:mm:ss.SSS ZZZ");
private static int numTimeZonesTested = 0;
private static int numTimeZonesPassed = 0;
private static int numTimeZonesFailed = 0;
private static int numTimeZonesException = 0;

private static String convertDateTimeToFormattedString(DateTime dateTime) {
    return formatter.print(dateTime);
}

private static DateTime parseStringToDateTime(String formattedDateTime) {
    return formatter.parseDateTime(formattedDateTime);
}

private static void testDateTimeFormatter(DateTime dateTime, String timeZone) {
    numTimeZonesTested++;

    final String dateTimeZoneId = dateTime.getZone().getID();

    if (!timeZone.equals(dateTimeZoneId)) {
        numTimeZonesFailed++;
        System.out.println(timeZone + " failed to construct into the proper date time zone - constructed time zone = " + dateTimeZoneId);
        return;
    }
    try {
        DateTime convertedDateTime = parseStringToDateTime(convertDateTimeToFormattedString(dateTime));

        if (dateTime.equals(convertedDateTime)) {
            numTimeZonesPassed++;
            //System.out.println(dateTime.getZone().getID() + " passed.");
        } else {
            numTimeZonesFailed++;
            System.out.println("Formatter failed for time zone ID: " + dateTimeZoneId + "    converted it to: " + convertedDateTime.getZone().getID());
        }
    } catch (IllegalArgumentException iae) {
        numTimeZonesException++;
        System.out.println("Formatter threw exception for time zone id: " + dateTimeZoneId);
    }
}

public static void main(String[] args) {
    Set<String> timeZones = DateTimeZone.getAvailableIDs();

    for (String timeZone : timeZones) {
        testDateTimeFormatter(DateTime.now().withZone(DateTimeZone.forID(timeZone)), timeZone);
    }

    System.out.println();
    System.out.println("Number of Time Zones tested: " + numTimeZonesTested);
    System.out.println("Number passed:     " + numTimeZonesPassed);
    System.out.println("Number failed:     " + numTimeZonesFailed);
    System.out.println("Number exceptions: " + numTimeZonesException);
    System.out.println();
}

}
The results are out of 572 time zones 130 fail and 30 throw exceptions.
The failures are the most interesting. When I query DateTimeZone to get its time zone ids I will get a time zone like America/Atka. When I take that id and create a date time with it its time zone id is America/Adak. It is like there are multiple list of time zones in Joda time and they are out of sync.
Source code is attached.


protected BasePeriod(long duration) {
    this(duration, null, null);
    // bug [3264409]
}

protected BasePeriod(long duration) {
    super();
    // bug [3264409]
    iType = PeriodType.time();
    int[] values = ISOChronology.getInstanceUTC().get(this, duration);
    iType = PeriodType.standard();
    iValues = new int[8];
    System.arraycopy(values, 0, iValues, 4, 4);
}

#113 Duration.toPeriod with fixed time zones.

I have a question concerning the conversion of a Duration to Period. I'm not sure if this is a bug, or if there is a different way to do this.
The basis of the problem, is that using Duration.toPeriod() uses the chronology of the default time zone to do the conversion. This can cause different results from a timezone with DST and one without. This can be reproduced easily with this test.
//set default time zone with this argument -Duser.timezone="GMT"
public void testForJodaForum()
{
    System.out.println("Timezone: " + DateTimeZone.getDefault());

    //Duration of more than 24 hours
    Duration aDuration = new Duration(DateTimeConstants.MILLIS_PER_HOUR * 30 + DateTimeConstants.MILLIS_PER_MINUTE * 50
        + DateTimeConstants.MILLIS_PER_SECOND * 14);

    System.out.println("Duration before: " + aDuration);
    Period period = aDuration.toPeriod();
    System.out.println("Period after: " + period);
}

A fixed time zone produces this output
Timezone: Etc/GMT
Duration before: PT111014S
Period after: P1DT6H50M14S
A DST time zone produces this output
Timezone: America/Chicago
Duration before: PT111014S
Period after: PT30H50M14S
In the joda code, Duration.toPeriod() uses a period constructor that takes the chronology, but null is passed in, so the chronology of the default time zone is used, which leads to this behavior.
The javadoc of toPeriod() states that only precise fields of hours, minutes, seconds, and millis will be converted. But for a fixed timezone, days and weeks are also precise, which is stated in the javadoc for toPeriod(Chronology chrono). In our app, we need consistent behavior regardless of the default time zone, which is to have all the extra hours put into the hours bucket. Since Duration is supposed to be a 'time zone independent' length of time, I don't think we should have to do any chronology manipulation to get this to work.
Any help is appreciated.
Thanks,
Cameron


private static synchronized String getConvertedId(String id) {
    Map<String, String> map = cZoneIdConversion;
    if (map == null) {
        // Backwards compatibility with TimeZone.
        map = new HashMap<String, String>();
        map.put("GMT", "UTC");
        map.put("MIT", "Pacific/Apia");
        map.put("HST", "Pacific/Honolulu");  // JDK 1.1 compatible
        map.put("AST", "America/Anchorage");
        map.put("PST", "America/Los_Angeles");
        map.put("MST", "America/Denver");  // JDK 1.1 compatible
        map.put("PNT", "America/Phoenix");
        map.put("CST", "America/Chicago");
        map.put("EST", "America/New_York");  // JDK 1.1 compatible
        map.put("IET", "America/Indianapolis");
        map.put("PRT", "America/Puerto_Rico");
        map.put("CNT", "America/St_Johns");
        map.put("AGT", "America/Buenos_Aires");
        map.put("BET", "America/Sao_Paulo");
        map.put("WET", "Europe/London");
        map.put("ECT", "Europe/Paris");
        map.put("ART", "Africa/Cairo");
        map.put("CAT", "Africa/Harare");
        map.put("EET", "Europe/Bucharest");
        map.put("EAT", "Africa/Addis_Ababa");
        map.put("MET", "Asia/Tehran");
        map.put("NET", "Asia/Yerevan");
        map.put("PLT", "Asia/Karachi");
        map.put("IST", "Asia/Calcutta");
        map.put("BST", "Asia/Dhaka");
        map.put("VST", "Asia/Saigon");
        map.put("CTT", "Asia/Shanghai");
        map.put("JST", "Asia/Tokyo");
        map.put("ACT", "Australia/Darwin");
        map.put("AET", "Australia/Sydney");
        map.put("SST", "Pacific/Guadalcanal");
        map.put("NST", "Pacific/Auckland");
        cZoneIdConversion = map;
    }
    return map.get(id);
}

private static synchronized String getConvertedId(String id) {
    Map<String, String> map = cZoneIdConversion;
    if (map == null) {
        // Backwards compatibility with TimeZone.
        map = new HashMap<String, String>();
        map.put("GMT", "UTC");
        map.put("WET", "WET");
        map.put("CET", "CET");
        map.put("MET", "CET");
        map.put("ECT", "CET");
        map.put("EET", "EET");
        map.put("MIT", "Pacific/Apia");
        map.put("HST", "Pacific/Honolulu");  // JDK 1.1 compatible
        map.put("AST", "America/Anchorage");
        map.put("PST", "America/Los_Angeles");
        map.put("MST", "America/Denver");  // JDK 1.1 compatible
        map.put("PNT", "America/Phoenix");
        map.put("CST", "America/Chicago");
        map.put("EST", "America/New_York");  // JDK 1.1 compatible
        map.put("IET", "America/Indiana/Indianapolis");
        map.put("PRT", "America/Puerto_Rico");
        map.put("CNT", "America/St_Johns");
        map.put("AGT", "America/Argentina/Buenos_Aires");
        map.put("BET", "America/Sao_Paulo");
        map.put("ART", "Africa/Cairo");
        map.put("CAT", "Africa/Harare");
        map.put("EAT", "Africa/Addis_Ababa");
        map.put("NET", "Asia/Yerevan");
        map.put("PLT", "Asia/Karachi");
        map.put("IST", "Asia/Kolkata");
        map.put("BST", "Asia/Dhaka");
        map.put("VST", "Asia/Ho_Chi_Minh");
        map.put("CTT", "Asia/Shanghai");
        map.put("JST", "Asia/Tokyo");
        map.put("ACT", "Australia/Darwin");
        map.put("AET", "Australia/Sydney");
        map.put("SST", "Pacific/Guadalcanal");
        map.put("NST", "Pacific/Auckland");
        cZoneIdConversion = map;
    }
    return map.get(id);
}

#112 Incorrect mapping of the MET time zone

This timezone is mapped to Asia/Tehran in DateTimeZone. It should be middle europena time.
I know that this bug has been raised before (Incorrect mapping of the MET time zone - ID: 2012274), and there is a comment stating that you won't break backward compatibility to fix this bug.

I disagree that this is a backward compatibility argument
No matter how you look at it, it is a bug.

You could very well state that ALL bugs won't be fixed, because of backward compatibility.
I request again that this bug be fixed.


public long computeMillis(boolean resetFields, String text) {
    SavedField[] savedFields = iSavedFields;
    int count = iSavedFieldsCount;
    if (iSavedFieldsShared) {
        iSavedFields = savedFields = (SavedField[])iSavedFields.clone();
        iSavedFieldsShared = false;
    }
    sort(savedFields, count);
    if (count > 0) {
        // alter base year for parsing if first field is month or day
        DurationField months = DurationFieldType.months().getField(iChrono);
        DurationField days = DurationFieldType.days().getField(iChrono);
        DurationField first = savedFields[0].iField.getDurationField();
        if (compareReverse(first, months) >= 0 && compareReverse(first, days) <= 0) {
            saveField(DateTimeFieldType.year(), iDefaultYear);
            return computeMillis(resetFields, text);
        }
    }

    long millis = iMillis;
    try {
        for (int i = 0; i < count; i++) {
            millis = savedFields[i].set(millis, resetFields);
        }
    } catch (IllegalFieldValueException e) {
        if (text != null) {
            e.prependMessage("Cannot parse \"" + text + '"');
        }
        throw e;
    }

    if (iZone == null) {
        millis -= iOffset;
    } else {
        int offset = iZone.getOffsetFromLocal(millis);
        millis -= offset;
        if (offset != iZone.getOffset(millis)) {
            String message =
                "Illegal instant due to time zone offset transition (" + iZone + ')';
            if (text != null) {
                message = "Cannot parse \"" + text + "\": " + message;
            }
            throw new IllegalArgumentException(message);
        }
    }

    return millis;
}

public long computeMillis(boolean resetFields, String text) {
    SavedField[] savedFields = iSavedFields;
    int count = iSavedFieldsCount;
    if (iSavedFieldsShared) {
        iSavedFields = savedFields = (SavedField[])iSavedFields.clone();
        iSavedFieldsShared = false;
    }
    sort(savedFields, count);
    if (count > 0) {
        // alter base year for parsing if first field is month or day
        DurationField months = DurationFieldType.months().getField(iChrono);
        DurationField days = DurationFieldType.days().getField(iChrono);
        DurationField first = savedFields[0].iField.getDurationField();
        if (compareReverse(first, months) >= 0 && compareReverse(first, days) <= 0) {
            saveField(DateTimeFieldType.year(), iDefaultYear);
            return computeMillis(resetFields, text);
        }
    }

    long millis = iMillis;
    try {
        for (int i = 0; i < count; i++) {
            millis = savedFields[i].set(millis, resetFields);
        }
        if (resetFields) {
            for (int i = 0; i < count; i++) {
                millis = savedFields[i].set(millis, i == (count - 1));
            }
        }
    } catch (IllegalFieldValueException e) {
        if (text != null) {
            e.prependMessage("Cannot parse \"" + text + '"');
        }
        throw e;
    }

    if (iZone == null) {
        millis -= iOffset;
    } else {
        int offset = iZone.getOffsetFromLocal(millis);
        millis -= offset;
        if (offset != iZone.getOffset(millis)) {
            String message =
                "Illegal instant due to time zone offset transition (" + iZone + ')';
            if (text != null) {
                message = "Cannot parse \"" + text + "\": " + message;
            }
            throw new IllegalArgumentException(message);
        }
    }

    return millis;
}

#107 Incorrect date parsed when week and month used together

I have following code snippet :
    DateTimeFormatter dtf = DateTimeFormat.forPattern("xxxxMM'w'ww");
DateTime dt = dtf.parseDateTime("201101w01");
System.out.println(dt);

It should print 2011-01-03 but it is printing 2010-01-04.
Please let me know if I am doing something wrong here.


public int getOffsetFromLocal(long instantLocal) {
    // get the offset at instantLocal (first estimate)
    final int offsetLocal = getOffset(instantLocal);
    // adjust instantLocal using the estimate and recalc the offset
    final long instantAdjusted = instantLocal - offsetLocal;
    final int offsetAdjusted = getOffset(instantAdjusted);
    // if the offsets differ, we must be near a DST boundary
    if (offsetLocal != offsetAdjusted) {
        // we need to ensure that time is always after the DST gap
        // this happens naturally for positive offsets, but not for negative
        if ((offsetLocal - offsetAdjusted) < 0) {
            // if we just return offsetAdjusted then the time is pushed
            // back before the transition, whereas it should be
            // on or after the transition
            long nextLocal = nextTransition(instantAdjusted);
            long nextAdjusted = nextTransition(instantLocal - offsetAdjusted);
            if (nextLocal != nextAdjusted) {
                return offsetLocal;
            }
        }
    }
    return offsetAdjusted;
}

public int getOffsetFromLocal(long instantLocal) {
    // get the offset at instantLocal (first estimate)
    final int offsetLocal = getOffset(instantLocal);
    // adjust instantLocal using the estimate and recalc the offset
    final long instantAdjusted = instantLocal - offsetLocal;
    final int offsetAdjusted = getOffset(instantAdjusted);
    // if the offsets differ, we must be near a DST boundary
    if (offsetLocal != offsetAdjusted) {
        // we need to ensure that time is always after the DST gap
        // this happens naturally for positive offsets, but not for negative
        if ((offsetLocal - offsetAdjusted) < 0) {
            // if we just return offsetAdjusted then the time is pushed
            // back before the transition, whereas it should be
            // on or after the transition
            long nextLocal = nextTransition(instantAdjusted);
            long nextAdjusted = nextTransition(instantLocal - offsetAdjusted);
            if (nextLocal != nextAdjusted) {
                return offsetLocal;
            }
        }
    } else if (offsetLocal > 0) {
        long prev = previousTransition(instantAdjusted);
        if (prev < instantAdjusted) {
            int offsetPrev = getOffset(prev);
            int diff = offsetPrev - offsetLocal;
            if (instantAdjusted - prev <= diff) {
                return offsetPrev;
            }
        }
    }
    return offsetAdjusted;
}

#90 DateTimeZone.getOffsetFromLocal error during DST transition

This may be a failure of my understanding, but the comments in DateTimeZone.getOffsetFromLocal lead me to believe that if an ambiguous local time is given, the offset corresponding to the later of the two possible UTC instants will be returned - i.e. the greater offset.
This doesn't appear to tally with my experience. In fall 2009, America/Los_Angeles changed from -7 to -8 at 2am wall time on November 11. Thus 2am became 1am - so 1:30am is ambiguous. I would therefore expect that constructing a DateTime for November 11th, 1:30am would give an instant corresponding with the later value (i.e. 9:30am UTC). This appears not to be the case:
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
public class TzTest {
public static void main(String[] args) throws Exception {
DateTimeZone zone = DateTimeZone.forID("America/Los_Angeles");
DateTime when1 = new DateTime(2009, 11, 1, 0, 30, 0, 0, zone);
DateTime when2 = new DateTime(2009, 11, 1, 1, 30, 0, 0, zone);
DateTime when3 = new DateTime(2009, 11, 1, 2, 30, 0, 0, zone);
System.out.println(when1);
System.out.println(when2);
System.out.println(when3);
}
}
Results:
2009-11-01T00:30:00.000-07:00 // Correct
2009-11-01T01:30:00.000-07:00 // Should be -08:00
2009-11-01T02:30:00.000-08:00 // Correct


private static PeriodFormatter toFormatter(List<Object> elementPairs, boolean notPrinter, boolean notParser) {
    if (notPrinter && notParser) {
        throw new IllegalStateException("Builder has created neither a printer nor a parser");
    }
    int size = elementPairs.size();
    if (size >= 2 && elementPairs.get(0) instanceof Separator) {
        Separator sep = (Separator) elementPairs.get(0);
            PeriodFormatter f = toFormatter(elementPairs.subList(2, size), notPrinter, notParser);
            sep = sep.finish(f.getPrinter(), f.getParser());
            return new PeriodFormatter(sep, sep);
    }
    Object[] comp = createComposite(elementPairs);
    if (notPrinter) {
        return new PeriodFormatter(null, (PeriodParser) comp[1]);
    } else if (notParser) {
        return new PeriodFormatter((PeriodPrinter) comp[0], null);
    } else {
        return new PeriodFormatter((PeriodPrinter) comp[0], (PeriodParser) comp[1]);
    }
}

private static PeriodFormatter toFormatter(List<Object> elementPairs, boolean notPrinter, boolean notParser) {
    if (notPrinter && notParser) {
        throw new IllegalStateException("Builder has created neither a printer nor a parser");
    }
    int size = elementPairs.size();
    if (size >= 2 && elementPairs.get(0) instanceof Separator) {
        Separator sep = (Separator) elementPairs.get(0);
        if (sep.iAfterParser == null && sep.iAfterPrinter == null) {
            PeriodFormatter f = toFormatter(elementPairs.subList(2, size), notPrinter, notParser);
            sep = sep.finish(f.getPrinter(), f.getParser());
            return new PeriodFormatter(sep, sep);
        }
    }
    Object[] comp = createComposite(elementPairs);
    if (notPrinter) {
        return new PeriodFormatter(null, (PeriodParser) comp[1]);
    } else if (notParser) {
        return new PeriodFormatter((PeriodPrinter) comp[0], null);
    } else {
        return new PeriodFormatter((PeriodPrinter) comp[0], (PeriodParser) comp[1]);
    }
}

#64 Different behaviour of PeriodFormatter

PeriodFormatter pfmt2 = pfmtbuilder2.append(ISOPeriodFormat.standard() ).toFormatter(); is not the same as
PeriodFormatterBuilder pfmtbuilder1 = new PeriodFormatterBuilder()
.appendLiteral("P")
.appendYears()
.appendSuffix("Y")
.appendMonths()
.appendSuffix("M")
.appendWeeks()
.appendSuffix("W")
.appendDays()
.appendSuffix("D")
.appendSeparatorIfFieldsAfter("T")
.appendHours()
.appendSuffix("H")
.appendMinutes()
.appendSuffix("M")
.appendSecondsWithOptionalMillis()
.appendSuffix("S");
which is copied from ISOPeriodFormat.standard() method


public Partial with(DateTimeFieldType fieldType, int value) {
    if (fieldType == null) {
        throw new IllegalArgumentException("The field type must not be null");
    }
    int index = indexOf(fieldType);
    if (index == -1) {
        DateTimeFieldType[] newTypes = new DateTimeFieldType[iTypes.length + 1];
        int[] newValues = new int[newTypes.length];

        // find correct insertion point to keep largest-smallest order
        int i = 0;
        DurationField unitField = fieldType.getDurationType().getField(iChronology);
        if (unitField.isSupported()) {
            for (; i < iTypes.length; i++) {
                DateTimeFieldType loopType = iTypes[i];
                DurationField loopUnitField = loopType.getDurationType().getField(iChronology);
                if (loopUnitField.isSupported()) {
                    int compare = unitField.compareTo(loopUnitField);
                    if (compare > 0) {
                        break;
                    } else if (compare == 0) {
                        DurationField rangeField = fieldType.getRangeDurationType().getField(iChronology);
                        DurationField loopRangeField = loopType.getRangeDurationType().getField(iChronology);
                        if (rangeField.compareTo(loopRangeField) > 0) {
                            break;
                        }
                    }
                }
            }
        }
        System.arraycopy(iTypes, 0, newTypes, 0, i);
        System.arraycopy(iValues, 0, newValues, 0, i);
        newTypes[i] = fieldType;
        newValues[i] = value;
        System.arraycopy(iTypes, i, newTypes, i + 1, newTypes.length - i - 1);
        System.arraycopy(iValues, i, newValues, i + 1, newValues.length - i - 1);
        // use public constructor to ensure full validation
        // this isn't overly efficient, but is safe
        Partial newPartial = new Partial(iChronology, newTypes, newValues);
        iChronology.validate(newPartial, newValues);
        return newPartial;
    }
    if (value == getValue(index)) {
        return this;
    }
    int[] newValues = getValues();
    newValues = getField(index).set(this, index, newValues, value);
    return new Partial(this, newValues);
}

public Partial with(DateTimeFieldType fieldType, int value) {
    if (fieldType == null) {
        throw new IllegalArgumentException("The field type must not be null");
    }
    int index = indexOf(fieldType);
    if (index == -1) {
        DateTimeFieldType[] newTypes = new DateTimeFieldType[iTypes.length + 1];
        int[] newValues = new int[newTypes.length];

        // find correct insertion point to keep largest-smallest order
        int i = 0;
        DurationField unitField = fieldType.getDurationType().getField(iChronology);
        if (unitField.isSupported()) {
            for (; i < iTypes.length; i++) {
                DateTimeFieldType loopType = iTypes[i];
                DurationField loopUnitField = loopType.getDurationType().getField(iChronology);
                if (loopUnitField.isSupported()) {
                    int compare = unitField.compareTo(loopUnitField);
                    if (compare > 0) {
                        break;
                    } else if (compare == 0) {
                        DurationField rangeField = fieldType.getRangeDurationType().getField(iChronology);
                        DurationField loopRangeField = loopType.getRangeDurationType().getField(iChronology);
                        if (rangeField.compareTo(loopRangeField) > 0) {
                            break;
                        }
                    }
                }
            }
        }
        System.arraycopy(iTypes, 0, newTypes, 0, i);
        System.arraycopy(iValues, 0, newValues, 0, i);
        newTypes[i] = fieldType;
        newValues[i] = value;
        System.arraycopy(iTypes, i, newTypes, i + 1, newTypes.length - i - 1);
        System.arraycopy(iValues, i, newValues, i + 1, newValues.length - i - 1);
        // use public constructor to ensure full validation
        // this isn't overly efficient, but is safe
        Partial newPartial = new Partial(newTypes, newValues, iChronology);
        iChronology.validate(newPartial, newValues);
        return newPartial;
    }
    if (value == getValue(index)) {
        return this;
    }
    int[] newValues = getValues();
    newValues = getField(index).set(this, index, newValues, value);
    return new Partial(this, newValues);
}

Partials can be constructed by invoking a constructor Partial(DateTimeFieldType[], int[]) or by merging together a set of partials using with, each constructed by calling Partial(DateTimeFieldType, int), e.g.:
Partial a = new Partial(new DateTimeFieldType[] { year(), hourOfDay() }, new int[] { 1, 1});
Partial b = new Partial(year(), 1).with(hourOfDay(), 1);
assert(a == b);
However, the above doesn't work in all cases:
new Partial(new DateTimeFieldType[] { clockhourOfDay(), hourOfDay() }, new int[] { 1, 1}); // throws Types array must not contain duplicate
new Partial(clockhourOfDay(), 1).with(hourOfDay(), 1); // #<Partial [clockhourOfDay=1, hourOfDay=1]>
I suppose the Partials should not allow to be constructed in either case. Is that right?
There's also a related issue (probably stems from the fact that the Partial is invalid):
new Partial(clockhourOfDay(), 1).with(hourOfDay(), 1).isEqual(new Partial(hourOfDay() ,1).with(clockhourOfDay(), 1)) // throws objects must have matching field types


public Period normalizedStandard(PeriodType type) {
    type = DateTimeUtils.getPeriodType(type);
    long millis = getMillis();  // no overflow can happen, even with Integer.MAX_VALUEs
    millis += (((long) getSeconds()) * ((long) DateTimeConstants.MILLIS_PER_SECOND));
    millis += (((long) getMinutes()) * ((long) DateTimeConstants.MILLIS_PER_MINUTE));
    millis += (((long) getHours()) * ((long) DateTimeConstants.MILLIS_PER_HOUR));
    millis += (((long) getDays()) * ((long) DateTimeConstants.MILLIS_PER_DAY));
    millis += (((long) getWeeks()) * ((long) DateTimeConstants.MILLIS_PER_WEEK));
    Period result = new Period(millis, type, ISOChronology.getInstanceUTC());
    int years = getYears();
    int months = getMonths();
    if (years != 0 || months != 0) {
        years = FieldUtils.safeAdd(years, months / 12);
        months = months % 12;
        if (years != 0) {
            result = result.withYears(years);
        }
        if (months != 0) {
            result = result.withMonths(months);
        }
    }
    return result;
}

public Period normalizedStandard(PeriodType type) {
    type = DateTimeUtils.getPeriodType(type);
    long millis = getMillis();  // no overflow can happen, even with Integer.MAX_VALUEs
    millis += (((long) getSeconds()) * ((long) DateTimeConstants.MILLIS_PER_SECOND));
    millis += (((long) getMinutes()) * ((long) DateTimeConstants.MILLIS_PER_MINUTE));
    millis += (((long) getHours()) * ((long) DateTimeConstants.MILLIS_PER_HOUR));
    millis += (((long) getDays()) * ((long) DateTimeConstants.MILLIS_PER_DAY));
    millis += (((long) getWeeks()) * ((long) DateTimeConstants.MILLIS_PER_WEEK));
    Period result = new Period(millis, type, ISOChronology.getInstanceUTC());
    int years = getYears();
    int months = getMonths();
    if (years != 0 || months != 0) {
        long totalMonths = years * 12L + months;
        if (type.isSupported(DurationFieldType.YEARS_TYPE)) {
            int normalizedYears = FieldUtils.safeToInt(totalMonths / 12);
            result = result.withYears(normalizedYears);
            totalMonths = totalMonths - (normalizedYears * 12);
        }
        if (type.isSupported(DurationFieldType.MONTHS_TYPE)) {
            int normalizedMonths = FieldUtils.safeToInt(totalMonths);
            result = result.withMonths(normalizedMonths);
            totalMonths = totalMonths - normalizedMonths;
        }
        if (totalMonths != 0) {
            throw new UnsupportedOperationException("Unable to normalize as PeriodType is missing either years or months but period has a month/year amount: " + toString());
        }
    }
    return result;
}

none standard PeriodType without year throws exception

Hi.
I tried to get a Period only for months and weeks with following code:
Period p = new Period(new DateTime(startDate.getTime()), new DateTime(endDate.getTime()), PeriodType.forFields(new DurationFieldType[]{DurationFieldType.months(), DurationFieldType.weeks()})).normalizedStandard(PeriodType.forFields(new DurationFieldType[]{DurationFieldType.months(), DurationFieldType.weeks()}));
return p.getMonths();
This throws following exception:
 10-17 14:35:50.999: E/AndroidRuntime(1350): java.lang.UnsupportedOperationException: Field is not supported
 10-17 14:35:50.999: E/AndroidRuntime(1350): at org.joda.time.PeriodType.setIndexedField(PeriodType.java:690)
 10-17 14:35:50.999: E/AndroidRuntime(1350): at org.joda.time.Period.withYears(Period.java:896) 10-17
 14:35:50.999: E/AndroidRuntime(1350): at org.joda.time.Period.normalizedStandard(Period.java:1630)

Even removing the year component with .withYearsRemoved() throws the same exception:
this works:
Period p = new Period(new DateTime(startDate.getTime()), new DateTime(endDate.getTime()), PeriodType.standard()).normalizedStandard(PeriodType.standard());
return p.getMonths();
this fails:
Period p = new Period(new DateTime(startDate.getTime()), new DateTime(endDate.getTime()), PeriodType.standard().withYearsRemoved()).normalizedStandard(PeriodType.standard().withYearsRemoved());
return p.getMonths();


public static DateTimeZone forOffsetHoursMinutes(int hoursOffset, int minutesOffset) throws IllegalArgumentException {
    if (hoursOffset == 0 && minutesOffset == 0) {
        return DateTimeZone.UTC;
    }
    if (hoursOffset < -23 || hoursOffset > 23) {
        throw new IllegalArgumentException("Hours out of range: " + hoursOffset);
    }
    if (minutesOffset < 0 || minutesOffset > 59) {
        throw new IllegalArgumentException("Minutes out of range: " + minutesOffset);
    }
    int offset = 0;
    try {
        int hoursInMinutes = hoursOffset * 60;
        if (hoursInMinutes < 0) {
            minutesOffset = hoursInMinutes - minutesOffset;
        } else {
            minutesOffset = hoursInMinutes + minutesOffset;
        }
        offset = FieldUtils.safeMultiply(minutesOffset, DateTimeConstants.MILLIS_PER_MINUTE);
    } catch (ArithmeticException ex) {
        throw new IllegalArgumentException("Offset is too large");
    }
    return forOffsetMillis(offset);
}

public static DateTimeZone forOffsetHoursMinutes(int hoursOffset, int minutesOffset) throws IllegalArgumentException {
    if (hoursOffset == 0 && minutesOffset == 0) {
        return DateTimeZone.UTC;
    }
    if (hoursOffset < -23 || hoursOffset > 23) {
        throw new IllegalArgumentException("Hours out of range: " + hoursOffset);
    }
    if (minutesOffset < -59 || minutesOffset > 59) {
        throw new IllegalArgumentException("Minutes out of range: " + minutesOffset);
    }
    if (hoursOffset > 0 && minutesOffset < 0) {
        throw new IllegalArgumentException("Positive hours must not have negative minutes: " + minutesOffset);
    }
    int offset = 0;
    try {
        int hoursInMinutes = hoursOffset * 60;
        if (hoursInMinutes < 0) {
            minutesOffset = hoursInMinutes - Math.abs(minutesOffset);
        } else {
            minutesOffset = hoursInMinutes + minutesOffset;
        }
        offset = FieldUtils.safeMultiply(minutesOffset, DateTimeConstants.MILLIS_PER_MINUTE);
    } catch (ArithmeticException ex) {
        throw new IllegalArgumentException("Offset is too large");
    }
    return forOffsetMillis(offset);
}

DateTimeZone.forOffsetHoursMinutes cannot handle negative offset < 1 hour

DateTimeZone.forOffsetHoursMinutes(h,m) cannot handle negative offset < 1 hour like -0:30 due to argument range checking. I used forOffsetMillis() instead.
This should probably be mentioned in the documentation or negative minutes be accepted.