public void testTimeZoneHelperFromXVCalendarBoston() throws Exception {
    Property tz = new Property("TZ", "-05");
    List<Property> dayLightList = new ArrayList<Property>();
    dayLightList.add(new Property("DAYLIGHT", "TRUE;-04;20070311T020000;20071104T020000;;"));
    dayLightList.add(new Property("DAYLIGHT", "TRUE;-04;20080309T020000;20081102T020000;;"));
    dayLightList.add(new Property("DAYLIGHT", "TRUE;-04;20090308T020000;20091101T020000;;"));
    TimeZoneHelper vctz = new TimeZoneHelper(tz, dayLightList);
    List<TimeZoneTransition> transitions = vctz.getTransitions();

    assertEquals(6, transitions.size());
    assertEquals(-18000000, vctz.getBasicOffset());
    for (TimeZoneTransition transition : transitions) {
      assertEquals("", transition.getName());
    }
    assertEquals(-14400000, transitions.get(0).getOffset());
    assertEquals(1173596400000L, transitions.get(0).getTime());
    assertEquals(-18000000, transitions.get(1).getOffset());
    assertEquals(1194159600000L, transitions.get(1).getTime());
    assertEquals(-14400000, transitions.get(2).getOffset());
    assertEquals(1205046000000L, transitions.get(2).getTime());
    assertEquals(-18000000, transitions.get(3).getOffset());
    assertEquals(1225609200000L, transitions.get(3).getTime());
    assertEquals(-14400000, transitions.get(4).getOffset());
    assertEquals(1236495600000L, transitions.get(4).getTime());
    assertEquals(-18000000, transitions.get(5).getOffset());
    assertEquals(1257058800000L, transitions.get(5).getTime());

    assertEquals("America/New_York", vctz.toID()); // First run
    assertEquals("America/New_York", vctz.toID()); // Second run (cached)
  }
  public void testTimeZoneHelperToICalendarCanberra() {

    TimeZoneHelper vctz =
        new TimeZoneHelper(
            "Australia/Canberra",
            1167609600000L, // 01 Jan 2007 @ 00:00:00 UTC
            1349049600000L); // 01 Oct 2012 @ 00:00:00 UTC
    VTimezone vtz = vctz.getVTimezone();
    List<VComponent> dayLightComponents = vtz.getComponents("DAYLIGHT");
    List<VComponent> standardComponents = vtz.getComponents("STANDARD");

    assertEquals("Australia/Canberra", vtz.getProperty("TZID").getValue());
    assertEquals(1, dayLightComponents.size());
    TzDaylightComponent dayLight = (TzDaylightComponent) dayLightComponents.get(0);
    assertEquals("+1000", dayLight.getProperty("TZOFFSETFROM").getValue());
    assertEquals("+1100", dayLight.getProperty("TZOFFSETTO").getValue());
    assertEquals("20061029T020000", dayLight.getProperty("DTSTART").getValue());
    assertEquals(
        "FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10", dayLight.getProperty("RRULE").getValue());

    assertEquals(1, standardComponents.size());
    TzStandardComponent standard = (TzStandardComponent) standardComponents.get(0);
    assertEquals("+1100", standard.getProperty("TZOFFSETFROM").getValue());
    assertEquals("+1000", standard.getProperty("TZOFFSETTO").getValue());
    assertEquals("20070325T030000", standard.getProperty("DTSTART").getValue());
    assertEquals(
        "FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3", standard.getProperty("RRULE").getValue());
  }
  public void testTimeZoneHelperToICalendarBogota() {

    TimeZoneHelper vctz =
        new TimeZoneHelper(
            "America/Bogota",
            0L, // 01 Jan 1970 @ 00:00:00 UTC
            1324771200000L); // 25 Dec 2011 @ 00:00:00 UTC

    VTimezone vtz = vctz.getVTimezone();
    List<VComponent> dayLightComponents = vtz.getComponents("DAYLIGHT");
    List<VComponent> standardComponents = vtz.getComponents("STANDARD");

    assertEquals("America/Bogota", vtz.getProperty("TZID").getValue());
    assertEquals(0, dayLightComponents.size());
    assertEquals(2, standardComponents.size());
    TzStandardComponent standard = (TzStandardComponent) standardComponents.get(0);
    assertEquals("-0500", standard.getProperty("TZOFFSETFROM").getValue());
    assertEquals("-0400", standard.getProperty("TZOFFSETTO").getValue());
    assertEquals("19920503T000000", standard.getProperty("DTSTART").getValue());
    assertEquals("19920503T000000", standard.getProperty("RDATE").getValue());

    standard = (TzStandardComponent) standardComponents.get(1);
    assertEquals("-0400", standard.getProperty("TZOFFSETFROM").getValue());
    assertEquals("-0500", standard.getProperty("TZOFFSETTO").getValue());
    assertEquals("19930404T000000", standard.getProperty("DTSTART").getValue());
    assertEquals("19930404T000000", standard.getProperty("RDATE").getValue());
  }
  public void testTimeZoneHelperFromICalendarBerlinUntil() throws Exception {

    VTimezone vtz = new VTimezone();
    vtz.addProperty("TZID", "Berlino"); // No hint
    TzStandardComponent standardC = new TzStandardComponent();
    standardC.addProperty("TZOFFSETFROM", "+0200");
    standardC.addProperty("TZOFFSETTO", "+0100");
    standardC.addProperty("TZNAME", "CET");
    standardC.addProperty("DTSTART", "19701025T030000");
    standardC.addProperty(
        "RRULE", "FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10;UNTIL=20091231T000113Z");
    TzDaylightComponent dayLightC = new TzDaylightComponent();
    dayLightC.addProperty("TZOFFSETFROM", "+0100");
    dayLightC.addProperty("TZOFFSETTO", "+0200");
    dayLightC.addProperty("TZNAME", "CEST");
    dayLightC.addProperty("DTSTART", "19700329T020000");
    dayLightC.addProperty(
        "RRULE", "FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3;UNTIL=20091231T000113Z");
    vtz.addStandardc(standardC);
    vtz.addDaylightc(dayLightC);

    TimeZoneHelper vctz =
        new TimeZoneHelper(
            vtz,
            1167609600000L, // 01 Jan 2007 @ 00:00:00 UTC
            1324771200000L); // 25 Dec 2011 @ 00:00:00 UTC
    List<TimeZoneTransition> transitions = vctz.getTransitions();

    assertEquals("Berlino", vctz.getName());
    assertEquals(6, transitions.size());
    assertEquals(3600000, vctz.getBasicOffset());
    boolean dst = true;
    for (TimeZoneTransition transition : transitions) {
      if (dst) {
        assertEquals("CEST", transition.getName());
      } else {
        assertEquals("CET", transition.getName());
      }
      dst = !dst;
    }
    assertEquals(7200000, transitions.get(0).getOffset());
    assertEquals(1174784400000L, transitions.get(0).getTime());
    assertEquals(3600000, transitions.get(1).getOffset());
    assertEquals(1193533200000L, transitions.get(1).getTime());
    assertEquals(7200000, transitions.get(2).getOffset());
    assertEquals(1206838800000L, transitions.get(2).getTime());
    assertEquals(3600000, transitions.get(3).getOffset());
    assertEquals(1224982800000L, transitions.get(3).getTime());
    assertEquals(7200000, transitions.get(4).getOffset());
    assertEquals(1238288400000L, transitions.get(4).getTime());
    assertEquals(3600000, transitions.get(5).getOffset());
    assertEquals(1256432400000L, transitions.get(5).getTime());

    assertEquals("Europe/Berlin", vctz.toID()); // First run
    assertEquals("Europe/Berlin", vctz.toID()); // Second run (cached)
  }
  public void testTimeZoneHelperFromOlsonIDBogota() {

    TimeZoneHelper vctz =
        new TimeZoneHelper(
            "America/Bogota",
            1167609600000L, // 01 Jan 2007 @ 00:00:00 UTC
            1349049600000L); // 01 Oct 2012 @ 00:00:00 UTC
    List<TimeZoneTransition> transitions = vctz.getTransitions();

    assertEquals("America/Bogota", vctz.toID());
    assertEquals(-18000000, vctz.getBasicOffset());
    assertEquals(0, transitions.size());
  }
  public void testTimeZoneHelperToXVCalendarBogota() {

    TimeZoneHelper vctz =
        new TimeZoneHelper(
            "America/Bogota",
            1167609600000L, // 01 Jan 2007 @ 00:00:00 UTC
            1349049600000L); // 01 Oct 2012 @ 00:00:00 UTC
    Property tz = vctz.getTZ();
    List<Property> dayLightList = vctz.getDaylightList();

    assertEquals(1, dayLightList.size());
    assertEquals("TZ", tz.getName());
    assertEquals("-0500", tz.getValue());
    assertEquals("DAYLIGHT", dayLightList.get(0).getName());
    assertEquals("FALSE", dayLightList.get(0).getValue());
  }
  public void testTimeZoneHelperFromOlsonIDCanberra() {

    TimeZoneHelper vctz =
        new TimeZoneHelper(
            "Australia/Canberra",
            1167609600000L, // 01 Jan 2007 @ 00:00:00 UTC
            1349049600000L); // 01 Oct 2012 @ 00:00:00 UTC
    List<TimeZoneTransition> transitions = vctz.getTransitions();

    assertEquals("Australia/Canberra", vctz.toID());
    assertEquals(36000000, vctz.getBasicOffset());
    assertEquals("Australia/Canberra", transitions.get(0).getName());
    assertEquals(39600000, transitions.get(0).getOffset());
    assertEquals(1162051200000L, transitions.get(0).getTime());
    assertEquals("Australia/Canberra", transitions.get(1).getName());
    assertEquals(36000000, transitions.get(1).getOffset());
    assertEquals(1174752000000L, transitions.get(1).getTime());
  }
  public void testTimeZoneHelperFromOlsonIDRome() {

    TimeZoneHelper vctz =
        new TimeZoneHelper(
            "Europe/Rome",
            1167609600000L, // 01 Jan 2007 @ 00:00:00 UTC
            1349049600000L); // 01 Oct 2012 @ 00:00:00 UTC
    List<TimeZoneTransition> transitions = vctz.getTransitions();

    assertEquals("Europe/Rome", vctz.toID());
    assertEquals(3600000, vctz.getBasicOffset());
    assertEquals("Europe/Rome", transitions.get(0).getName());
    assertEquals(7200000, transitions.get(0).getOffset());
    assertEquals(1174784400000L, transitions.get(0).getTime());
    assertEquals("Europe/Rome", transitions.get(1).getName());
    assertEquals(3600000, transitions.get(1).getOffset());
    assertEquals(1193533200000L, transitions.get(1).getTime());
  }
  public void testTimeZoneHelperToICalendarJohannesburg() {

    TimeZoneHelper vctz =
        new TimeZoneHelper(
            "Africa/Johannesburg",
            0L, // 01 Jan 1970 @ 00:00:00 UTC
            1324771200000L); // 25 Dec 2011 @ 00:00:00 UTC

    VTimezone vtz = vctz.getVTimezone();
    List<VComponent> dayLightComponents = vtz.getComponents("DAYLIGHT");
    List<VComponent> standardComponents = vtz.getComponents("STANDARD");

    assertEquals("Africa/Johannesburg", vtz.getProperty("TZID").getValue());
    assertEquals(0, dayLightComponents.size());
    assertEquals(1, standardComponents.size());
    TzStandardComponent standard = (TzStandardComponent) standardComponents.get(0);
    assertEquals("+0200", standard.getProperty("TZOFFSETFROM").getValue());
    assertEquals("+0200", standard.getProperty("TZOFFSETTO").getValue());
    assertEquals("19700101T000000", standard.getProperty("DTSTART").getValue());
    assertEquals("19700101T000000", standard.getProperty("RDATE").getValue());
  }
  // --------------------------------------------------------------- Test cases
  public void testTimeZoneHelperFromXVCalendarRome() throws Exception {
    Property tz = new Property("TZ", "+01");
    List<Property> dayLightList = new ArrayList<Property>();
    dayLightList.add(new Property("DAYLIGHT", "TRUE;+02;20070325T010000Z;20071028T010000Z;;"));
    dayLightList.add(new Property("DAYLIGHT", "TRUE;+02;20080330T010000Z;20081026T010000Z;;"));
    dayLightList.add(new Property("DAYLIGHT", "TRUE;+02;20090329T010000Z;20091025T010000Z;;"));
    dayLightList.add(new Property("DAYLIGHT", "TRUE;+02;20100328T010000Z;20101031T010000Z;;"));
    dayLightList.add(new Property("DAYLIGHT", "TRUE;+02;20110327T010000Z;20111030T010000Z;;"));
    TimeZoneHelper vctz = new TimeZoneHelper(tz, dayLightList);
    List<TimeZoneTransition> transitions = vctz.getTransitions();

    assertEquals(10, transitions.size());
    assertEquals(3600000, vctz.getBasicOffset());
    for (TimeZoneTransition transition : transitions) {
      assertEquals("", transition.getName());
    }
    assertEquals(7200000, transitions.get(0).getOffset());
    assertEquals(1174784400000L, transitions.get(0).getTime());
    assertEquals(3600000, transitions.get(1).getOffset());
    assertEquals(1193533200000L, transitions.get(1).getTime());
    assertEquals(7200000, transitions.get(2).getOffset());
    assertEquals(1206838800000L, transitions.get(2).getTime());
    assertEquals(3600000, transitions.get(3).getOffset());
    assertEquals(1224982800000L, transitions.get(3).getTime());
    assertEquals(7200000, transitions.get(4).getOffset());
    assertEquals(1238288400000L, transitions.get(4).getTime());
    assertEquals(3600000, transitions.get(5).getOffset());
    assertEquals(1256432400000L, transitions.get(5).getTime());
    assertEquals(7200000, transitions.get(6).getOffset());
    assertEquals(1269738000000L, transitions.get(6).getTime());
    assertEquals(3600000, transitions.get(7).getOffset());
    assertEquals(1288486800000L, transitions.get(7).getTime());
    assertEquals(7200000, transitions.get(8).getOffset());
    assertEquals(1301187600000L, transitions.get(8).getTime());
    assertEquals(3600000, transitions.get(9).getOffset());
    assertEquals(1319936400000L, transitions.get(9).getTime());

    assertEquals("Europe/Berlin", vctz.toID()); // First run
    assertEquals("Europe/Berlin", vctz.toID()); // Second run (cached)
  }
  public void testTimeZoneHelperToXVCalendarCanberra() {

    TimeZoneHelper vctz =
        new TimeZoneHelper(
            "Australia/Canberra",
            1167609600000L, // 01 Jan 2007 @ 00:00:00 UTC
            1349049600000L); // 01 Oct 2012 @ 00:00:00 UTC
    Property tz = vctz.getTZ();
    List<Property> dayLightList = vctz.getDaylightList();

    assertEquals(6, dayLightList.size());
    assertEquals("TZ", tz.getName());
    assertEquals("+1000", tz.getValue());
    assertEquals("DAYLIGHT", dayLightList.get(0).getName());
    assertEquals(
        "TRUE;+1100;20061029T020000;20070325T030000;Australia/Canberra;Australia/Canberra",
        dayLightList.get(0).getValue());
    assertEquals("DAYLIGHT", dayLightList.get(1).getName());
    assertEquals(
        "TRUE;+1100;20071028T020000;20080330T030000;Australia/Canberra;Australia/Canberra",
        dayLightList.get(1).getValue());
    assertEquals("DAYLIGHT", dayLightList.get(2).getName());
    assertEquals(
        "TRUE;+1100;20081026T020000;20090329T030000;Australia/Canberra;Australia/Canberra",
        dayLightList.get(2).getValue());
    assertEquals("DAYLIGHT", dayLightList.get(3).getName());
    assertEquals(
        "TRUE;+1100;20091025T020000;20100328T030000;Australia/Canberra;Australia/Canberra",
        dayLightList.get(3).getValue());
    assertEquals("DAYLIGHT", dayLightList.get(4).getName());
    assertEquals(
        "TRUE;+1100;20101031T020000;20110327T030000;Australia/Canberra;Australia/Canberra",
        dayLightList.get(4).getValue());
    assertEquals("DAYLIGHT", dayLightList.get(5).getName());
    assertEquals(
        "TRUE;+1100;20111030T020000;20120325T030000;Australia/Canberra;Australia/Canberra",
        dayLightList.get(5).getValue());
  }
  @SuppressWarnings("unchecked")
  public void testTimeZoneHelperCheckAllOlsonIDsWithICalendar() throws Exception {

    int mistakes = 0; // counter for mistakes in time zone handling

    /*
     * Max number of mistakes allowed. This value should be lowered as soon
     * as improvements in the code that handles time zones reduce the
     * number of mistakes. In this way, this test can be used to certify
     * that the number of properly handled time zones is being constantly
     * raised.
     */
    final int MAX_MISTAKES = 125;

    System.out.println("================================= iCalendar (2.0)");
    for (String id : (Set<String>) DateTimeZone.getAvailableIDs()) {

      TimeZoneHelper vctzOut =
          new TimeZoneHelper(
              id,
              // 0L,              // 01 Jan 1970 @ 00:00:00 UTC
              915148800000L, // 01 Jan 1999 @ 00:00:00 UTC
              1324771200000L); // 25 Dec 2011 @ 00:00:00 UTC
      VTimezone vTimezone = vctzOut.getVTimezone();

      // Uncomment this to debug the iCalendar items referring to one
      // continent:
      /*
      String continent = "Australia"; // or whatever you want to watch
      if (id.startsWith(continent)) {
          System.out.println("\n\n" + vTimezone);
      }
      */

      vTimezone.getProperty("TZID").setValue(""); // The name should not be used as a hint for
      // this "test"

      TimeZoneHelper vctzIn =
          new TimeZoneHelper(
              vTimezone,
              915148800000L, // 01 Jan 1999 @ 00:00:00 UTC
              1324771200000L); // 25 Dec 2011 @ 00:00:00 UTC
      System.out.print('\n' + id + " --> " + vctzIn.toID());
      vctzIn.clearCachedID();
      String guess = vctzIn.toID(id);
      if (!id.equals(guess)) {
        System.out.print(" (WRONG!)");
        mistakes++;
      }
    }
    System.out.println("\n\n" + mistakes + " time zones were not properly handled.");
    assertTrue(mistakes <= MAX_MISTAKES);
    System.out.println();
  }
  @SuppressWarnings("unchecked")
  public void testTimeZoneHelperCheckAllOlsonIDsWithXVCalendar() throws Exception {

    int mistakes = 0; // counter for mistakes in time zone handling

    /*
     * Max number of mistakes allowed. This value should be lowered as soon
     * as improvements in the code that handles time zones reduce the
     * number of mistakes. In this way, this test can be used to certify
     * that the number of properly handled time zones is being constantly
     * raised.
     */
    final int MAX_MISTAKES = 16;

    System.out.println("================================= vCalendar (1.0)");
    for (String id : (Set<String>) DateTimeZone.getAvailableIDs()) {

      TimeZoneHelper vctzOut =
          new TimeZoneHelper(
              id,
              // 0L,              // 01 Jan 1970 @ 00:00:00 UTC
              915148800000L, // 01 Jan 1999 @ 00:00:00 UTC
              1324771200000L); // 25 Dec 2011 @ 00:00:00 UTC
      List<Property> dayLightList = vctzOut.getDaylightList();
      Property tz = vctzOut.getTZ();

      TimeZoneHelper vctzIn = new TimeZoneHelper(tz, dayLightList);
      vctzIn.setName(""); // The name should not be used as a hint for
      // this "test"
      System.out.print('\n' + id + " --> " + vctzIn.toID());
      vctzIn.clearCachedID();
      String guess = vctzIn.toID(id);
      if (!id.equals(guess)) {
        System.out.print(" (WRONG!)");
        mistakes++;
      }
    }
    System.out.println("\n\n" + mistakes + " time zones were not properly handled.");
    assertTrue(mistakes <= MAX_MISTAKES);
    System.out.println();
  }
/**
 * This class implements the time-zone conversions.
 *
 * @version $Id: TimeZoneHelper.java,v 1.7 2008-08-27 10:58:39 mauro Exp $
 */
public class TimeZoneHelper {

  private boolean cachedID = false;
  protected String id = null;

  private final DateFormat DF = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
  private final DateFormat DF_NO_Z = new SimpleDateFormat("yyyyMMdd'T'HHmmss");

  private static final DecimalFormat HH = new DecimalFormat("+00;-00");
  private static final DecimalFormat MM = new DecimalFormat("00");
  private static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC");

  private static final long NINE_MONTHS = 23328000000L; // 270 days
  private static final long THREE_MONTHS = 7776000000L; // 90 days

  private static String[] FAVORITE_TIME_ZONE_IDS = {
    "Etc/UTC",
    "Europe/Berlin",
    "Europe/London",
    "Europe/Moscow",
    "Europe/Istanbul",
    "America/Los_Angeles",
    "America/New_York",
    "America/Phoenix",
    "America/Denver",
    "Africa/Tunis",
    "Africa/Lagos",
    "Africa/Johannesburg",
    "Africa/Nairobi",
    "America/Mexico_City",
    "America/La_Paz",
    "America/Tijuana",
    "America/Buenos_Aires",
    "America/La_Rioja",
    "America/Port-au-Prince",
    "America/Sao_Paulo",
    "Asia/Tel_Aviv",
    "Asia/Bangkok",
    "Asia/Shanghai",
    "Asia/Dacca",
    "Asia/Phnom_Penh",
    "Asia/Riyadh",
    "Asia/Dubai",
    "Asia/Tokyo",
    "Asia/Tashkent",
    "Asia/Vladivostok",
    "Australia/Adelaide",
    "Australia/Brisbane",
    "Australia/Canberra",
    "Australia/Darwin",
    "Australia/Hobart",
    "Australia/Sydney",
    "Antarctica/South_Pole"
  };

  private Pattern OLSON_ID_PATTERN =
      Pattern.compile(
          "(Europe|A((meric)|(si)|(fric)|(ustrali)|(ntarctic))a|Pacific|Atlantic)"
              + "/[A-Z]([A-Z,a-z,_,',\\-])+"
              + "(/[A-Z]([A-Z,a-z,_,',\\-])+)?");

  // --------------------------------------------------------------- Properties

  private String name = null;
  private int basicOffset; // in milliseconds
  private List<TimeZoneTransition> transitions = new ArrayList<TimeZoneTransition>();

  private static long referenceTime = -1L;
  private final long REFERENCE_TIME = TimeZoneHelper.getReferenceTime();

  protected String getName() {
    return name;
  }

  /**
   * This setter is only for test purposes. Usually, the name is set by constructors.
   *
   * @param name the new name to set
   */
  protected void setName(String name) {
    this.name = name;
  }

  protected int getBasicOffset() {
    return basicOffset;
  }

  protected List<TimeZoneTransition> getTransitions() {
    return transitions;
  }

  // No need for setters

  // ------------------------------------------------------------- Constructors

  /** Just creates an empty TimeZoneHelper. It's only for usage by subclasses. */
  protected TimeZoneHelper() {
    setFormattersToUTC();

    // Does nothing.
  }

  /**
   * Creates a new instance of TimeZoneHelper on the basis of the information extracted from a
   * vCalendar (1.0) item.
   *
   * @param tz the TZ property
   * @param daylightList a List containing all DAYLIGHT properties
   * @throws java.lang.Exception
   */
  public TimeZoneHelper(Property tz, List<Property> daylightList) throws Exception {
    setFormattersToUTC();
    this.name = ""; // vCalendar (1.0) has no time zone identifier because
    // there's just one time zone per calendar item

    if ((tz == null) || (tz.getValue() == null) || (tz.getValue().length() == 0)) {
      throw new Exception("No TZ property");
    }

    basicOffset = parseOffset(tz.getValue());

    for (Property transition : daylightList) {

      if (transition.getValue() == null) {
        continue;
      }
      if (transition.getValue().startsWith("TRUE;")) {
        String[] daylight = transition.getValue().split(";");

        String summerOffsetString = daylight[1].replaceAll("[\\+\\-:]", "") + "00";

        int summerOffset = 3600000 * Integer.parseInt(summerOffsetString.substring(0, 2));
        summerOffset += 60000 * Integer.parseInt(summerOffsetString.substring(2, 4));
        if (daylight[1].startsWith("-")) {
          summerOffset = -summerOffset;
        }

        long summerStart = parseDateTime(daylight[2]);
        String summerTimeName;
        if (daylight.length >= 5) {
          summerTimeName = daylight[4];
        } else {
          summerTimeName = "";
        }
        TimeZoneTransition summerTime =
            new TimeZoneTransition(summerOffset, summerStart, summerTimeName);

        long summerEnd = parseDateTime(daylight[3]);
        String standardTimeName;
        if (daylight.length >= 6) {
          standardTimeName = daylight[5];
        } else {
          standardTimeName = "";
        }
        TimeZoneTransition standardTime =
            new TimeZoneTransition(basicOffset, summerEnd, standardTimeName);

        transitions.add(summerTime);
        transitions.add(standardTime);
      }
    }
  }

  /**
   * Creates a new instance of TimeZoneHelper on the basis of the information extracted from a SIF-E
   * or SIF-T item.
   *
   * @param sifTimeZone the Timezone node of a SIF-E or SIF-T item
   * @throws java.lang.Exception
   */
  public TimeZoneHelper(Node sifTimeZone) throws Exception {
    setFormattersToUTC();
    this.name = ""; // vCalendar (1.0) has no time zone identifier because
    // there's just one time zone per calendar item

    NodeList timeZoneSubcomponents = sifTimeZone.getChildNodes();
    List<Node> sifDayLights = new ArrayList<Node>(timeZoneSubcomponents.getLength() - 1);
    String tz = null;
    for (int i = 0; i < timeZoneSubcomponents.getLength(); i++) {

      Node child = timeZoneSubcomponents.item(i);
      if (!(child instanceof Element)) {
        continue;
      }

      if (SIFCalendar.TIME_ZONE_BASIC_OFFSET.equals(child.getNodeName())) {
        tz = child.getTextContent();
      } else {
        sifDayLights.add(child);
      }
    }

    if (tz == null) {
      throw new Exception("No BasicOffset tag");
    }

    basicOffset = parseOffset(tz);

    // @todo Manage no-DST case

    for (Node dayLight : sifDayLights) {

      Integer summerOffset = null;
      Long summerStart = null;
      Long summerEnd = null;
      String summerTimeName = "";
      String standardTimeName = "";

      NodeList dayLightSubcomponents = dayLight.getChildNodes();
      for (int i = 0; i < dayLightSubcomponents.getLength(); i++) {

        Node child = dayLightSubcomponents.item(i);
        if (!(child instanceof Element)) {
          continue;
        }

        if (SIFCalendar.TIME_ZONE_DST_OFFSET.equals(child.getNodeName())) {
          String summerOffsetString =
              child.getTextContent().replaceAll("[\\+\\-:]", "") + "00"; // More robust
          summerOffset = 3600000 * Integer.parseInt(summerOffsetString.substring(0, 2));
          summerOffset += 60000 * Integer.parseInt(summerOffsetString.substring(2, 4));
          if (child.getTextContent().startsWith("-")) {
            summerOffset = -summerOffset;
          }
        } else if (SIFCalendar.TIME_ZONE_DST_START.equals(child.getNodeName())) {
          summerStart = parseDateTime(child.getTextContent());
        } else if (SIFCalendar.TIME_ZONE_DST_END.equals(child.getNodeName())) {
          summerEnd = parseDateTime(child.getTextContent());
        } else if (SIFCalendar.TIME_ZONE_STANDARD_NAME.equals(child.getNodeName())) {
          standardTimeName = child.getTextContent();
        } else if (SIFCalendar.TIME_ZONE_DST_NAME.equals(child.getNodeName())) {
          summerTimeName = child.getTextContent();
        }
      }

      TimeZoneTransition summerTime =
          new TimeZoneTransition(summerOffset, summerStart, summerTimeName);

      TimeZoneTransition standardTime =
          new TimeZoneTransition(basicOffset, summerEnd, standardTimeName);

      transitions.add(summerTime);
      transitions.add(standardTime);
    }
  }

  /**
   * Creates a new instance of TimeZoneHelper on the basis of the information extracted from an
   * iCalendar (vCalendar 2.0) item.
   *
   * @param vTimeZone
   * @param from the start of the relevant time interval for the generation of transitions (an
   *     istant expressed as a long)
   * @param to the end of the relevant time interval for the generation of transitions (an istant
   *     expressed as a long)
   * @throws java.lang.Exception
   */
  public TimeZoneHelper(VTimezone vTimeZone, long from, long to) throws Exception {
    setFormattersToUTC();
    Property tzID = vTimeZone.getProperty("TZID");
    if (tzID != null) {
      this.name = tzID.getValue();

      // Try and skip the parsing by using just the TZID:
      String extracted = extractID(name);
      if (extracted != null) {
        cacheID(extracted);
        processID(extracted, from, to);
        return;
      }

      List<VComponent> standardTimeRules = vTimeZone.getComponents("STANDARD");
      List<VComponent> summerTimeRules = vTimeZone.getComponents("DAYLIGHT");

      String standardTimeOffset;
      if (standardTimeRules.isEmpty()) {
        if (summerTimeRules.isEmpty()) {
          throw new Exception("Empty VTIMEZONE");
        } else {
          standardTimeOffset = summerTimeRules.get(0).getProperty("TZOFFSETFROM").getValue();
        }
      } else {
        standardTimeOffset = standardTimeRules.get(0).getProperty("TZOFFSETTO").getValue();
      }
      basicOffset = parseOffset(standardTimeOffset);

      for (VComponent standardTimeRule : standardTimeRules) {
        addTransitions(standardTimeRule, from, to);
      }
      for (VComponent summerTimeRule : summerTimeRules) {
        addTransitions(summerTimeRule, from, to);
      }
      Collections.sort(transitions);

    } else {
      this.name = ""; // This should not happen!
    }
  }

  /**
   * Creates a new instance of TimeZoneHelper on the basis of a zoneinfo (Olson database) ID.
   *
   * @param id the time zone ID according to the zoneinfo (Olson) database
   * @param from the start of the relevant time interval for the generation of transitions (an
   *     istant expressed as a long)
   * @param to the end of the relevant time interval for the generation of transitions (an istant
   *     expressed as a long)
   */
  public TimeZoneHelper(String id, long from, long to) {
    setFormattersToUTC();
    cacheID(id);
    processID(id, from, to);
  }

  /**
   * Extract time-zone information from a zoneinfo (Olson database) ID and saves them in the
   * TimeZoneHelper fields.
   *
   * @param id the time zone ID according to the zoneinfo (Olson) database
   * @param from the start of the relevant time interval for the generation of transitions (an
   *     istant expressed as a long)
   * @param to the end of the relevant time interval for the generation of transitions (an istant
   *     expressed as a long)
   */
  protected void processID(String id, long from, long to) {

    DateTimeZone tz = DateTimeZone.forID(id);
    if (name == null) { // The name could have been set already using TZID
      // and in this case it is important not to change it
      name = id; // The Olson ID is perfect as a unique name
    }
    basicOffset = tz.getStandardOffset(from);
    transitions.clear();

    if (!tz.isFixed()) {

      long oldFrom = from;
      from = fixFrom(tz, basicOffset, oldFrom);

      // @todo Consider case when to go beyond last transition (cycle
      // could become endless)
      while (tz.getStandardOffset(to) != tz.getOffset(to)) {
        to = tz.nextTransition(to);
      }

      while ((from <= to) && (oldFrom != from)) {
        transitions.add(new TimeZoneTransition(tz.getOffset(from), from, id));
        oldFrom = from;
        from = tz.nextTransition(oldFrom);
      }
    }
  }

  // ----------------------------------------------------------- Public methods

  /**
   * Gets an Olson ID corresponding to the transitions and offsets saved. First it looks for a
   * cached ID. If it is not found, it looks for a matching ID among the favorite ones. Otherwise it
   * looks for it among all available IDs with the same basic offset.
   *
   * @return a string containing the Olson ID or null if no matching ID is found
   */
  public String toID() {

    if (cachedID) {
      return id;
    }

    for (String idGuess : FAVORITE_TIME_ZONE_IDS) {
      if (matchesID(idGuess)) {
        return cacheID(idGuess);
      }
    }

    for (String idGuess : TimeZone.getAvailableIDs(getBasicOffset())) {
      if (matchesID(idGuess)) {
        return cacheID(idGuess);
      }
    }
    return cacheID(null); // No matching time zone found
  }

  /**
   * Gets an Olson ID corresponding to the information saved and a suggestion. First it looks for a
   * cached ID. If it is not found, it checks if the saved name simply contains an Olson ID. If it
   * does, it will be returned as the result without further investigation. If that is not the case,
   * the suggested ID is checked against the transitions and offsets saved. If this also fails, it
   * looks for a matching ID among the favorite ones. Otherwise it looks for it among all available
   * IDs with the same basic offset.
   *
   * @param suggested the suggested ID (as a string)
   * @return a string containing the Olson ID or null if no matching ID is found
   */
  public String toID(String suggested) {

    if (cachedID) {
      return id;
    }
    String extractedID = extractID(name);
    if (extractedID != null) {
      return cacheID(extractedID);
    }
    if ((suggested != null) && (matchesID(suggested))) {
      return cacheID(suggested);
    }
    return toID();
  }

  /**
   * Gets an Olson ID corresponding to the information saved and a suggestion. First it looks for a
   * cached ID. If it is not found, it checks if the saved name simply contains an Olson ID. If it
   * does, it will be returned as the result without further investigation. If that is not the case,
   * the ID of the suggested time zone is checked against the transitions and offsets saved. If this
   * also fails, it looks for a matching ID among the favorite ones. Otherwise it looks for it among
   * all available IDs with the same basic offset.
   *
   * @param suggested the suggested time zone (as a TimeZone object)
   * @return a string containing the Olson ID or null if no matching ID is found
   */
  public String toID(TimeZone suggested) {

    if (suggested != null) {
      return toID(suggested.getID());
    }
    return toID((String) null);
  }

  public Property getTZ() {
    return new Property("TZ", formatOffset(getBasicOffset()));
  }

  public List<Property> getDaylightList() {

    List<Property> properties = new ArrayList<Property>(getTransitions().size() / 2);

    if (getTransitions().size() == 0) {
      properties.add(new Property("DAYLIGHT", "FALSE"));
    }

    // @todo Check the case with an odd number of transitions
    int previousOffset = getBasicOffset();
    for (int i = 0; i < getTransitions().size() - 1; i += 2) {

      TimeZoneTransition transitionToSummerTime = getTransitions().get(i);
      TimeZoneTransition transitionToStandardTime = getTransitions().get(i + 1);
      Date forth = new Date(transitionToSummerTime.getTime() + previousOffset);
      Date back = new Date(transitionToStandardTime.getTime() + transitionToSummerTime.getOffset());
      previousOffset = transitionToStandardTime.getOffset();

      StringBuffer buffer = new StringBuffer("TRUE;");
      buffer
          .append(formatOffset(transitionToSummerTime.getOffset()))
          .append(';')
          .append(DF_NO_Z.format(forth))
          .append(';')
          .append(DF_NO_Z.format(back))
          .append(';')
          .append(transitionToStandardTime.getName())
          .append(';')
          .append(transitionToSummerTime.getName());
      properties.add(new Property("DAYLIGHT", buffer.toString()));
    }

    return properties;
  }

  public List<Property> getXVCalendarProperties() {
    List<Property> properties = new ArrayList<Property>();
    properties.add(getTZ());
    properties.addAll(getDaylightList());

    return properties;
  }

  public VTimezone getVTimezone() {

    return toVTimezone(getICalendarTransitions(), getName(), getBasicOffset());
  }

  public String getSIF() {
    StringBuffer xml = new StringBuffer();
    List<Property> xvCalendarProperties = getXVCalendarProperties();

    openXMLTag(xml, SIFCalendar.TIME_ZONE);
    for (Property property : xvCalendarProperties) {
      if ("TZ".equals(property.getName())) {
        addXMLNode(xml, SIFCalendar.TIME_ZONE_BASIC_OFFSET, property.getValue());
      }

      if ("DAYLIGHT".equals(property.getName())) {
        String dl = property.getValue();
        if (!"FALSE".equalsIgnoreCase(dl)) {

          openXMLTag(xml, SIFCalendar.TIME_ZONE_DAYLIGHT);
          String[] dlParts = dl.split(";");

          addXMLNode(xml, SIFCalendar.TIME_ZONE_DST_OFFSET, dlParts[1]);
          addXMLNode(xml, SIFCalendar.TIME_ZONE_DST_START, dlParts[2]);
          addXMLNode(xml, SIFCalendar.TIME_ZONE_DST_END, dlParts[3]);
          addXMLNode(xml, SIFCalendar.TIME_ZONE_DST_NAME, (dlParts[4] == null) ? "" : dlParts[4]);
          addXMLNode(
              xml, SIFCalendar.TIME_ZONE_STANDARD_NAME, (dlParts[5] == null) ? "" : dlParts[5]);
          closeXMLTag(xml, SIFCalendar.TIME_ZONE_DAYLIGHT);
        } else {
          // When DAYLIGHT is FALSE does not send <DayLight/>
        }
      }
    }

    closeXMLTag(xml, SIFCalendar.TIME_ZONE);
    return xml.toString();
  }

  // -------------------------------------------------------- Protected methods

  protected List<ICalendarTimeZoneTransition> getICalendarTransitions() {

    List<ICalendarTimeZoneTransition> iCalendarTransitions;

    if (getTransitions().isEmpty()) {
      iCalendarTransitions = new ArrayList<ICalendarTimeZoneTransition>(1);
      iCalendarTransitions.add(new ICalendarTimeZoneTransition(getName(), getBasicOffset()));
      return iCalendarTransitions;
    }

    iCalendarTransitions = new ArrayList<ICalendarTimeZoneTransition>(getTransitions().size());

    int previousOffset = getBasicOffset();
    for (TimeZoneTransition transition : getTransitions()) {
      iCalendarTransitions.add(new ICalendarTimeZoneTransition(transition, previousOffset));
      previousOffset = transition.getOffset();
    }

    return iCalendarTransitions;
  }

  protected String cacheID(String id) {
    cachedID = true;
    this.id = id;
    return id;
  }

  protected static VTimezone toVTimezone(
      List<ICalendarTimeZoneTransition> iCalendarTransitions, String id, int basicOffset) {

    VTimezone vtz = new VTimezone();
    vtz.addProperty("TZID", id);
    TzDaylightComponent summerTimeRDates = null;
    TzStandardComponent standardTimeRDates = null;
    String standardTimeOffset = formatOffset(basicOffset);

    // Visits all transitions in cronological order
    for (int i = 0; i < iCalendarTransitions.size(); ) {

      // If it's the last transition, or it's a transition that is not
      // part of the standard/day-light time series, the special case must
      // be separately treated
      if ((i == iCalendarTransitions.size() - 1)
          || (!areHalfYearFar(
              iCalendarTransitions.get(i).getTime(), iCalendarTransitions.get(i + 1).getTime()))) {

        // "Burns" components that may be present in the buffer
        if (summerTimeRDates != null) {
          vtz.addComponent(summerTimeRDates);
          vtz.addComponent(standardTimeRDates);
          summerTimeRDates = null;
          standardTimeRDates = null;
        }

        // Creates a new STANDARD component of the RDATE kind
        TzStandardComponent specialRDate = new TzStandardComponent();
        String specialCaseTime = iCalendarTransitions.get(i).getTimeISO1861();
        specialRDate.addProperty("DTSTART", specialCaseTime);
        specialRDate.addProperty("RDATE", specialCaseTime);
        specialRDate.addProperty("TZOFFSETFROM", standardTimeOffset);
        standardTimeOffset = // It needs be updated
            formatOffset(iCalendarTransitions.get(i).getOffset());
        specialRDate.addProperty("TZOFFSETTO", standardTimeOffset);
        specialRDate.addProperty("TZNAME", iCalendarTransitions.get(i).getName());
        vtz.addComponent(specialRDate); // Burns it
        i++; // Moves on to the next transition
        continue;
      }

      String lastOffset = standardTimeOffset;
      String summerTimeOffset = formatOffset(iCalendarTransitions.get(i).getOffset());
      standardTimeOffset = formatOffset(iCalendarTransitions.get(i + 1).getOffset());
      String summerTimeStart = iCalendarTransitions.get(i).getTimeISO1861();
      String standardTimeStart = iCalendarTransitions.get(i + 1).getTimeISO1861();
      ICalendarTimeZoneTransition summerTimeClusterStart = iCalendarTransitions.get(i);
      ICalendarTimeZoneTransition standardTimeClusterStart = iCalendarTransitions.get(i + 1);
      int j; // Summer-time starts, backward instance count
      int k; // Summer-time starts, forward instance count
      int l; // Summer-time ends, backward instance count
      int m; // Summer-time ends, forward instance count
      for (j = i + 2; j < iCalendarTransitions.size(); j += 2) {
        ICalendarTimeZoneTransition clusterMember = iCalendarTransitions.get(j);
        if (!summerTimeClusterStart.matchesRecurrence(clusterMember, true)) {
          break;
        }
      }
      for (k = i + 2; k < iCalendarTransitions.size(); k += 2) {
        ICalendarTimeZoneTransition clusterMember = iCalendarTransitions.get(k);
        if (!summerTimeClusterStart.matchesRecurrence(clusterMember, false)) {
          break;
        }
      }
      for (l = i + 3; l < iCalendarTransitions.size(); l += 2) {
        ICalendarTimeZoneTransition clusterMember = iCalendarTransitions.get(l);
        if (!standardTimeClusterStart.matchesRecurrence(clusterMember, true)) {
          break;
        }
      }
      for (m = i + 3; m < iCalendarTransitions.size(); m += 2) {
        ICalendarTimeZoneTransition clusterMember = iCalendarTransitions.get(m);
        if (!standardTimeClusterStart.matchesRecurrence(clusterMember, false)) {
          break;
        }
      }
      boolean backwardInstanceCountForStarts = true;
      boolean backwardInstanceCountForEnds = true;
      if (k > j) { // counting istances in the forward direction makes a
        // longer summer-time-start series
        j = k; // j is now the longest series of summer-time starts
        backwardInstanceCountForStarts = false;
      }
      if (m > l) { // counting istances in the forward direction makes a
        // longer summer-time-end series
        l = m; // l is now the longest series of summer-time ends
        backwardInstanceCountForEnds = false;
      }
      j -= 2; // Compensates for the end condition of the for cycle above
      l -= 2; // Compensates for the end condition of the for cycle above
      if (l > j + 1) {
        l = j + 1; // l is now the best acceptable end for a
        // combined start-end series
      } else {
        j = l - 1;
      }
      // At this point, l + 1 = j

      if (l > i + 1) { // more than one year: there's a recurrence
        if (summerTimeRDates != null) {
          vtz.addComponent(summerTimeRDates);
          vtz.addComponent(standardTimeRDates);
        }

        // Create a new DAYLIGHT component
        summerTimeRDates = new TzDaylightComponent();
        summerTimeRDates.addProperty("DTSTART", summerTimeStart);
        StringBuffer summerTimeRRule = new StringBuffer("FREQ=YEARLY;INTERVAL=1;BYDAY=");
        if (backwardInstanceCountForStarts) {
          summerTimeRRule.append("-1");
        } else {
          summerTimeRRule.append('+').append(summerTimeClusterStart.getInstance());
        }
        summerTimeRRule
            .append(getDayOfWeekAbbreviation(summerTimeClusterStart.getDayOfWeek()))
            .append(";BYMONTH=")
            .append(summerTimeClusterStart.getMonth() + 1); // Jan must be 1
        if (j < iCalendarTransitions.size() - 2) {
          summerTimeRRule.append(";UNTIL=").append(iCalendarTransitions.get(j).getTimeISO1861());
        }
        summerTimeRDates.addProperty("RRULE", summerTimeRRule.toString());
        summerTimeRDates.addProperty("TZOFFSETFROM", lastOffset);
        summerTimeRDates.addProperty("TZOFFSETTO", summerTimeOffset);
        summerTimeRDates.addProperty("TZNAME", iCalendarTransitions.get(i).getName());

        // Create a new STANDARD component
        standardTimeRDates = new TzStandardComponent();
        standardTimeRDates.addProperty("DTSTART", standardTimeStart);
        StringBuffer standardTimeRRule = new StringBuffer("FREQ=YEARLY;INTERVAL=1;BYDAY=");
        if (backwardInstanceCountForEnds) {
          standardTimeRRule.append("-1");
        } else {
          standardTimeRRule.append('+').append(standardTimeClusterStart.getInstance());
        }
        standardTimeRRule
            .append(getDayOfWeekAbbreviation(standardTimeClusterStart.getDayOfWeek()))
            .append(";BYMONTH=")
            .append(standardTimeClusterStart.getMonth() + 1); // Jan must be 1
        if (l < iCalendarTransitions.size() - 1) {
          standardTimeRRule.append(";UNTIL=").append(iCalendarTransitions.get(l).getTimeISO1861());
        }

        standardTimeRDates.addProperty("RRULE", standardTimeRRule.toString());
        standardTimeRDates.addProperty("TZOFFSETFROM", summerTimeOffset);
        standardTimeRDates.addProperty("TZOFFSETTO", standardTimeOffset);
        standardTimeRDates.addProperty("TZNAME", iCalendarTransitions.get(i + 1).getName());

        vtz.addComponent(summerTimeRDates);
        vtz.addComponent(standardTimeRDates);
        summerTimeRDates = null;
        standardTimeRDates = null;
        i = j; // Increases the counter to jump beyond the recurrence
      } else { // just one year: i, i+1 are transitions of the RDATE kind
        if (summerTimeRDates == null) {
          // Create a new DAYLIGHT component
          summerTimeRDates = new TzDaylightComponent();
          summerTimeRDates.addProperty("DTSTART", summerTimeStart);
          summerTimeRDates.addProperty("RDATE", summerTimeStart);
          summerTimeRDates.addProperty("TZOFFSETFROM", lastOffset);
          summerTimeRDates.addProperty("TZOFFSETTO", summerTimeOffset);
          summerTimeRDates.addProperty("TZNAME", iCalendarTransitions.get(i).getName());
          // Create a new STANDARD component
          standardTimeRDates = new TzStandardComponent();
          standardTimeRDates.addProperty("DTSTART", standardTimeStart);
          standardTimeRDates.addProperty("RDATE", standardTimeStart);
          standardTimeRDates.addProperty("TZOFFSETFROM", summerTimeOffset);
          standardTimeRDates.addProperty("TZOFFSETTO", standardTimeOffset);
          standardTimeRDates.addProperty("TZNAME", iCalendarTransitions.get(i + 1).getName());
        } else {
          Property rdate = summerTimeRDates.getProperty("RDATE");
          rdate.setValue(rdate.getValue() + ';' + summerTimeStart);
          summerTimeRDates.setProperty(rdate);
          rdate = standardTimeRDates.getProperty("RDATE");
          rdate.setValue(rdate.getValue() + ';' + standardTimeStart);
          standardTimeRDates.setProperty(rdate);
        }
      }
      i += 2;
    }
    if (summerTimeRDates != null) {
      vtz.addComponent(summerTimeRDates);
      vtz.addComponent(standardTimeRDates);
      summerTimeRDates = null;
      standardTimeRDates = null;
    }
    return vtz;
  }

  // ---------------------------------------------------------- Private methods

  private boolean matchesID(String idToCheck) {

    DateTimeZone tz;

    try {
      tz = DateTimeZone.forID(idToCheck);
    } catch (IllegalArgumentException e) { // the ID is not recognized
      return false;
    }

    if (getTransitions().size() == 0) { // No transitions
      if (tz.getStandardOffset(REFERENCE_TIME) != basicOffset) {
        return false; // Offsets don't match: wrong guess
      }
      if (tz.isFixed() || (REFERENCE_TIME == tz.nextTransition(REFERENCE_TIME))) {
        return true; // A right fixed or currently-fixed time zone
        // has been found
      }
      return false; // Wrong guess
    }

    long t = getTransitions().get(0).getTime() - 1;
    if (tz.getStandardOffset(t) != basicOffset) {
      return false; // Wrong guess
    }

    for (TimeZoneTransition transition : getTransitions()) {
      t = tz.nextTransition(t);
      if (!isClose(t, transition.getTime())) {
        return false; // Wrong guess
      }
      if (tz.getOffset(t) != transition.getOffset()) {
        return false; // Wrong guess
      }
    }
    return true; // A right non-fixed time zone has been found
  }

  private static String formatOffset(int offset) {

    int offsetHours = offset / 3600000;
    int offsetMinutes = (offset / 60000) % 60;

    return (HH.format(offsetHours) + MM.format(offsetMinutes));
  }

  /**
   * Parses offset string value that could be in the format prefix + or -, or with semicolon +03:00
   *
   * @param text the offset string value
   * @return the offset int value
   */
  private static int parseOffset(String text) throws Exception {
    int offset;
    try {
      String offsetString = text.replaceAll("[\\+\\-\\:]", "") + "00";
      offset = 3600000 * Integer.parseInt(offsetString.substring(0, 2));
      offset += 60000 * Integer.parseInt(offsetString.substring(2, 4));
      if (text.startsWith("-")) {
        return -offset;
      }
      return offset;
    } catch (Exception e) {
      throw new Exception("Wrong offset format");
    }
  }

  private long parseDateTime(String dateTime) throws ParseException {

    if (!dateTime.endsWith("Z")) {
      return DF_NO_Z.parse(dateTime).getTime() - getBasicOffset();
    }
    return DF.parse(dateTime).getTime();
  }

  private static boolean isClose(long t1, long t2) {
    if (t1 == t2) {
      return true;
    }
    long difference = t2 - t1;
    if ((difference <= 3600000) && (difference >= -3600000)) {
      return true;
    }
    return false;
  }

  private void addTransitions(VComponent timeRule, long from, long to) throws Exception {

    int offset;
    int previousOffset;
    String start;
    long startTime;
    long time;
    String transitionName;

    Property tzName = timeRule.getProperty("TZNAME");
    Property tzOffsetFrom = timeRule.getProperty("TZOFFSETFROM");
    Property tzOffsetTo = timeRule.getProperty("TZOFFSETTO");
    Property tzDtStart = timeRule.getProperty("DTSTART");
    Property tzRRule = timeRule.getProperty("RRULE");
    Property tzRDate = timeRule.getProperty("RDATE");

    if (tzDtStart != null) {
      start = tzDtStart.getValue();
      startTime = parseDateTime(start);
    } else {
      throw new Exception("Required property DTSTART (of a time zone) is missing");
    }
    if (tzOffsetTo != null) {
      offset = parseOffset(tzOffsetTo.getValue());
    } else {
      throw new Exception("Required property TZOFFSETTO is missing");
    }
    if (tzOffsetFrom != null) {
      previousOffset = parseOffset(tzOffsetFrom.getValue());
    } else {
      throw new Exception("Required property TZOFFSETFROM is missing");
    }
    if (tzName != null) {
      transitionName = tzName.getValue();
    } else {
      transitionName = "";
    }

    if (tzRDate != null) {
      String[] rDates = tzRDate.getValue().split(",");
      for (String rDate : rDates) {
        time = parseDateTime(rDate);
        transitions.add(new TimeZoneTransition(offset, time, transitionName));
      }
    }

    if (tzRRule != null) {
      RecurrencePattern rrule =
          VCalendarContentConverter.getRecurrencePattern(
              start,
              null,
              tzRRule.getValue(),
              null, // as of specs
              false); // iCalendar
      if (((rrule.getTypeId() == RecurrencePattern.TYPE_MONTH_NTH) && (rrule.getInterval() == 12))
          || ((rrule.getTypeId() == RecurrencePattern.TYPE_YEAR_NTH)
              && (rrule.getInterval() == 1))) { // yearly

        int dayOfWeek = getDayOfWeekFromMask(rrule.getDayOfWeekMask());
        if (dayOfWeek > 0) { // one day
          TimeZone fixed = TimeZone.getTimeZone("UTC");
          fixed.setRawOffset(previousOffset);
          Calendar finder = new GregorianCalendar(fixed);
          finder.setTimeInMillis(startTime); // Sets hour and minutes
          int hh = finder.get(Calendar.HOUR_OF_DAY);
          int mm = finder.get(Calendar.MINUTE);
          int m = rrule.getMonthOfYear() - 1; // Yes, it works
          int yearStart = year(startTime);
          int yearFrom = (startTime > from) ? yearStart : year(from);
          int yearTo = year(to);
          if (rrule.isNoEndDate()) {
            int count = rrule.getOccurrences();
            int yearRecurrenceEnd;
            if (count != -1) {
              yearRecurrenceEnd = yearStart + count - 1;
              if (yearRecurrenceEnd < yearTo) {
                yearTo = yearRecurrenceEnd;
              }
            }
          } else {
            try {
              int yearRecurrenceEnd = year(rrule.getEndDatePattern());
              if (yearRecurrenceEnd < yearTo) {
                yearTo = yearRecurrenceEnd;
              }
            } catch (ParseException e) {
              // Ignores the UNTIL part
            }
          }
          for (int y = yearFrom; y <= yearTo; y++) {
            finder.clear();
            finder.set(Calendar.YEAR, y);
            finder.set(Calendar.MONTH, m);
            finder.set(Calendar.DAY_OF_WEEK, dayOfWeek);
            finder.set(Calendar.DAY_OF_WEEK_IN_MONTH, rrule.getInstance());
            finder.set(Calendar.HOUR_OF_DAY, hh);
            finder.set(Calendar.MINUTE, mm);
            long transitionTime = finder.getTimeInMillis() - (previousOffset - getBasicOffset());
            transitions.add(new TimeZoneTransition(offset, transitionTime, transitionName));
          }
        }
      }
    }
  }

  protected static int year(long time) {
    final Calendar FINDER = new GregorianCalendar(UTC_TIME_ZONE);
    FINDER.setTimeInMillis(time);
    return FINDER.get(Calendar.YEAR);
  }

  protected static int year(String time) throws ParseException {

    String year = time.substring(0, 4);
    return Integer.parseInt(year);
  }

  private static int getDayOfWeekFromMask(short mask) {
    switch (mask) {
      case RecurrencePattern.DAY_OF_WEEK_SUNDAY:
        return Calendar.SUNDAY;
      case RecurrencePattern.DAY_OF_WEEK_MONDAY:
        return Calendar.MONDAY;
      case RecurrencePattern.DAY_OF_WEEK_TUESDAY:
        return Calendar.TUESDAY;
      case RecurrencePattern.DAY_OF_WEEK_WEDNESDAY:
        return Calendar.WEDNESDAY;
      case RecurrencePattern.DAY_OF_WEEK_THURSDAY:
        return Calendar.THURSDAY;
      case RecurrencePattern.DAY_OF_WEEK_FRIDAY:
        return Calendar.FRIDAY;
      case RecurrencePattern.DAY_OF_WEEK_SATURDAY:
        return Calendar.SATURDAY;
      case 0: // empty mask
        return 0;
      default: // several days
        return -1;
    }
  }

  private static String getDayOfWeekAbbreviation(int day) {
    switch (day) {
      case java.util.Calendar.SUNDAY:
        return "SU";
      case java.util.Calendar.SATURDAY:
        return "SA";
      case java.util.Calendar.FRIDAY:
        return "FR";
      case java.util.Calendar.THURSDAY:
        return "TH";
      case java.util.Calendar.WEDNESDAY:
        return "WE";
      case java.util.Calendar.TUESDAY:
        return "TU";
      case java.util.Calendar.MONDAY:
        return "MO";
      default: // empty mask or several days
        return null;
    }
  }

  public void clearCachedID() {
    cachedID = false;
  }

  public static long getReferenceTime() {
    if (referenceTime >= 0) {
      return referenceTime;
    } else {
      return System.currentTimeMillis();
    }
  }

  public static synchronized void setReferenceTime(long time) {
    if (time < 0) {
      referenceTime = -1;
    } else {
      referenceTime = time;
    }
  }

  private static boolean areHalfYearFar(long time0, long time1) {
    return ((time1 - time0 < NINE_MONTHS) && (time1 - time0 > THREE_MONTHS));
  }

  private void setFormattersToUTC() {
    DF.setTimeZone(TimeZone.getTimeZone("UTC"));
    DF_NO_Z.setTimeZone(TimeZone.getTimeZone("UTC"));
  }

  /** Returns a string with the node text content. */
  private String getNodeContent(Node node) {
    NodeList children = node.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      Node child = children.item(i);
      if (child instanceof Text) {
        return (child.getNodeValue());
      }
    }
    return ("");
  }

  /** Appends a opening XML tag. */
  private StringBuffer openXMLTag(StringBuffer buffer, String tag) {
    return buffer.append('<').append(tag).append('>');
  }

  /** Appends a closing XML tag. */
  private StringBuffer closeXMLTag(StringBuffer buffer, String tag) {
    return buffer.append("</").append(tag).append('>');
  }

  /** Appends an XML node with the given content. */
  private StringBuffer addXMLNode(StringBuffer buffer, String tag, String content) {

    if (content == null || "null".equals(content)) {
      return buffer.append('<').append(tag).append("/>");
    }
    return closeXMLTag(openXMLTag(buffer, tag).append(content), tag);
  }

  /**
   * Looks for a substring that corresponds to an Olson ID.
   *
   * @param label the string to search through
   * @return the substring that represents an Olson ID
   */
  private String extractID(String label) {
    Matcher matcher = OLSON_ID_PATTERN.matcher(label);
    if (matcher.find()) {
      String id = matcher.group();
      try {
        DateTimeZone.forID(id); // just to check whether it exists
      } catch (IllegalArgumentException e) { // not found
        return null;
      }
      return id;
    }
    return null;
  }

  protected long fixFrom(DateTimeZone tz, int standardOffset, long from) {

    if (standardOffset != tz.getOffset(from)) { // NB: this must NOT be
      // a call to getBasicOffset(), because that method may be
      // overriden by the cached implementation(s) of this class.
      do {
        from = tz.previousTransition(from) + 1;
      } while ((from != 0) && (standardOffset == tz.getOffset(from)));
    } else {
      from = tz.nextTransition(from);
    }
    return from;
  }
}