make/tools/src/build/tools/tzdb/TzdbZoneRulesCompiler.java

Print this page




 210         System.out.println("   -dstdir <directory>   Where to output generated files (default srcdir)");
 211         System.out.println("   -version <version>    Specify the version, such as 2009a (optional)");
 212         System.out.println("   -help                 Print this usage message");
 213         System.out.println("   -verbose              Output verbose information during compilation");
 214         System.out.println(" There must be one directory for each version in srcdir");
 215         System.out.println(" Each directory must have the name of the version, such as 2009a");
 216         System.out.println(" Each directory must contain the unpacked tzdb files, such as asia or europe");
 217         System.out.println(" Directories must match the regex [12][0-9][0-9][0-9][A-Za-z0-9._-]+");
 218         System.out.println(" There will be one jar file for each version and one combined jar in dstdir");
 219         System.out.println(" If the version is specified, only that version is processed");
 220     }
 221 
 222     /**
 223      * Process to create the jar files.
 224      */
 225     private static void process(List<File> srcDirs, List<String> srcFileNames, File dstDir, String version, boolean verbose) {
 226         // build actual jar files
 227         Map<String, SortedMap<String, ZoneRules>> allBuiltZones = new TreeMap<>();
 228         Set<String> allRegionIds = new TreeSet<String>();
 229         Set<ZoneRules> allRules = new HashSet<ZoneRules>();

 230 
 231         for (File srcDir : srcDirs) {
 232             // source files in this directory
 233             List<File> srcFiles = new ArrayList<>();
 234             for (String srcFileName : srcFileNames) {
 235                 File file = new File(srcDir, srcFileName);
 236                 if (file.exists()) {
 237                     srcFiles.add(file);
 238                 }
 239             }
 240             if (srcFiles.isEmpty()) {
 241                 continue;  // nothing to process
 242             }
 243 
 244             // compile
 245             String loopVersion = srcDir.getName();

 246             TzdbZoneRulesCompiler compiler = new TzdbZoneRulesCompiler(loopVersion, srcFiles, verbose);
 247             try {
 248                 // compile
 249                 compiler.compile();
 250                 SortedMap<String, ZoneRules> builtZones = compiler.getZones();
 251 
 252                 // output version-specific file
 253                 File dstFile = version == null ? new File(dstDir, "tzdb" + loopVersion + ".jar")
 254                                                : new File(dstDir, "tzdb.jar");
 255                 if (verbose) {
 256                     System.out.println("Outputting file: " + dstFile);
 257                 }
 258                 outputFile(dstFile, loopVersion, builtZones);
 259 
 260                 // create totals
 261                 allBuiltZones.put(loopVersion, builtZones);
 262                 allRegionIds.addAll(builtZones.keySet());
 263                 allRules.addAll(builtZones.values());

 264             } catch (Exception ex) {
 265                 System.out.println("Failed: " + ex.toString());
 266                 ex.printStackTrace();
 267                 System.exit(1);
 268             }
 269         }
 270 
 271         // output merged file
 272         if (version == null) {
 273             File dstFile = new File(dstDir, "tzdb-all.jar");
 274             if (verbose) {
 275                 System.out.println("Outputting combined file: " + dstFile);
 276             }
 277             outputFile(dstFile, allBuiltZones, allRegionIds, allRules);
 278         }
 279     }
 280 
 281     /**
 282      * Outputs the file.
 283      */
 284     private static void outputFile(File dstFile,
 285                                    String version,
 286                                    SortedMap<String, ZoneRules> builtZones) {

 287         Map<String, SortedMap<String, ZoneRules>> loopAllBuiltZones = new TreeMap<>();
 288         loopAllBuiltZones.put(version, builtZones);
 289         Set<String> loopAllRegionIds = new TreeSet<String>(builtZones.keySet());
 290         Set<ZoneRules> loopAllRules = new HashSet<ZoneRules>(builtZones.values());
 291         outputFile(dstFile, loopAllBuiltZones, loopAllRegionIds, loopAllRules);


 292     }
 293 
 294     /**
 295      * Outputs the file.
 296      */
 297     private static void outputFile(File dstFile,
 298                                    Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
 299                                    Set<String> allRegionIds,
 300                                    Set<ZoneRules> allRules)
 301     {
 302         try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(dstFile))) {
 303             outputTZEntry(jos, allBuiltZones, allRegionIds, allRules);
 304         } catch (Exception ex) {
 305             System.out.println("Failed: " + ex.toString());
 306             ex.printStackTrace();
 307             System.exit(1);
 308         }
 309     }
 310 
 311     /**
 312      * Outputs the timezone entry in the JAR file.
 313      */
 314     private static void outputTZEntry(JarOutputStream jos,
 315                                       Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
 316                                       Set<String> allRegionIds,
 317                                       Set<ZoneRules> allRules) {

 318         // this format is not publicly specified
 319         try {
 320             jos.putNextEntry(new ZipEntry("TZDB.dat"));
 321             DataOutputStream out = new DataOutputStream(jos);
 322 
 323             // file version
 324             out.writeByte(1);
 325             // group
 326             out.writeUTF("TZDB");
 327             // versions
 328             String[] versionArray = allBuiltZones.keySet().toArray(new String[allBuiltZones.size()]);
 329             out.writeShort(versionArray.length);
 330             for (String version : versionArray) {
 331                 out.writeUTF(version);
 332             }
 333             // regions
 334             String[] regionArray = allRegionIds.toArray(new String[allRegionIds.size()]);
 335             out.writeShort(regionArray.length);
 336             for (String regionId : regionArray) {
 337                 out.writeUTF(regionId);


 342             ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
 343             for (ZoneRules rules : rulesList) {
 344                 baos.reset();
 345                 DataOutputStream dataos = new DataOutputStream(baos);
 346                 rules.writeExternal(dataos);
 347                 dataos.close();
 348                 byte[] bytes = baos.toByteArray();
 349                 out.writeShort(bytes.length);
 350                 out.write(bytes);
 351             }
 352             // link version-region-rules
 353             for (String version : allBuiltZones.keySet()) {
 354                 out.writeShort(allBuiltZones.get(version).size());
 355                 for (Map.Entry<String, ZoneRules> entry : allBuiltZones.get(version).entrySet()) {
 356                      int regionIndex = Arrays.binarySearch(regionArray, entry.getKey());
 357                      int rulesIndex = rulesList.indexOf(entry.getValue());
 358                      out.writeShort(regionIndex);
 359                      out.writeShort(rulesIndex);
 360                 }
 361             }










 362             out.flush();
 363             jos.closeEntry();
 364         } catch (Exception ex) {
 365             System.out.println("Failed: " + ex.toString());
 366             ex.printStackTrace();
 367             System.exit(1);
 368         }
 369     }
 370 
 371     //-----------------------------------------------------------------------
 372     /** The TZDB rules. */
 373     private final Map<String, List<TZDBRule>> rules = new HashMap<>();
 374 
 375     /** The TZDB zones. */
 376     private final Map<String, List<TZDBZone>> zones = new HashMap<>();
 377     /** The TZDB links. */
 378 
 379     private final Map<String, String> links = new HashMap<>();
 380 
 381     /** The built zones. */


 604                 }
 605                 mdt.dayOfMonth = Integer.parseInt(dayRule);
 606             }
 607             if (st.hasMoreTokens()) {
 608                 String timeStr = st.nextToken();
 609                 int secsOfDay = parseSecs(timeStr);
 610                 if (secsOfDay == 86400) {
 611                     mdt.endOfDay = true;
 612                     secsOfDay = 0;
 613                 }
 614                 LocalTime time = LocalTime.ofSecondOfDay(secsOfDay);
 615                 mdt.time = time;
 616                 mdt.timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
 617             }
 618         }
 619     }
 620 
 621     private int parseYear(String str, int defaultYear) {
 622         if (YEAR.reset(str).matches()) {
 623             if (YEAR.group("min") != null) {
 624                 return YEAR_MIN_VALUE;

 625             } else if (YEAR.group("max") != null) {
 626                 return YEAR_MAX_VALUE;
 627             } else if (YEAR.group("only") != null) {
 628                 return defaultYear;
 629             }
 630             return Integer.parseInt(YEAR.group("year"));
 631         }
 632         throw new IllegalArgumentException("Unknown year: " + str);
 633     }
 634 
 635     private int parseMonth(String str) {
 636         if (MONTH.reset(str).matches()) {
 637             for (int moy = 1; moy < 13; moy++) {
 638                 if (MONTH.group(moy) != null) {
 639                     return moy;
 640                 }
 641             }
 642         }
 643         throw new IllegalArgumentException("Unknown month: " + str);
 644     }


 725             ZoneRulesBuilder bld = new ZoneRulesBuilder();
 726             for (TZDBZone tzdbZone : tzdbZones) {
 727                 bld = tzdbZone.addToBuilder(bld, rules);
 728             }
 729             ZoneRules buildRules = bld.toRules(zoneId);
 730             builtZones.put(zoneId, buildRules);
 731         }
 732 
 733         // build aliases
 734         for (String aliasId : links.keySet()) {
 735             String realId = links.get(aliasId);
 736             printVerbose("Linking alias " + aliasId + " to " + realId);
 737             ZoneRules realRules = builtZones.get(realId);
 738             if (realRules == null) {
 739                 realId = links.get(realId);  // try again (handle alias liked to alias)
 740                 printVerbose("Relinking alias " + aliasId + " to " + realId);
 741                 realRules = builtZones.get(realId);
 742                 if (realRules == null) {
 743                     throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId + "' for '" + version + "'");
 744                 }


 745             }
 746             builtZones.put(aliasId, realRules);
 747         }
 748 
 749         // remove UTC and GMT
 750         builtZones.remove("UTC");
 751         builtZones.remove("GMT");
 752         builtZones.remove("GMT0");
 753         builtZones.remove("GMT+0");
 754         builtZones.remove("GMT-0");


 755     }
 756 
 757     //-----------------------------------------------------------------------
 758     /**
 759      * Prints a verbose message.
 760      *
 761      * @param message  the message, not null
 762      */
 763     private void printVerbose(String message) {
 764         if (verbose) {
 765             System.out.println(message);
 766         }
 767     }
 768 
 769     //-----------------------------------------------------------------------
 770     /**
 771      * Class representing a month-day-time in the TZDB file.
 772      */
 773     abstract class TZDBMonthDayTime {
 774         /** The month of the cutover. */
 775         int month = 1;
 776         /** The day-of-month of the cutover. */
 777         int dayOfMonth = 1;
 778         /** Whether to adjust forwards. */
 779         boolean adjustForwards = true;
 780         /** The day-of-week of the cutover. */
 781         int dayOfWeek = -1;
 782         /** The time of the cutover. */
 783         LocalTime time = LocalTime.MIDNIGHT;
 784         /** Whether this is midnight end of day. */
 785         boolean endOfDay;
 786         /** The time of the cutover. */
 787         TimeDefinition timeDefinition = TimeDefinition.WALL;
 788 
 789         void adjustToFowards(int year) {
 790             if (adjustForwards == false && dayOfMonth > 0) {
 791                 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
 792                 dayOfMonth = adjustedDate.getDayOfMonth();
 793                 month = adjustedDate.getMonth();
 794                 adjustForwards = true;
 795             }
 796         }
 797     }
 798 
 799     /**
 800      * Class representing a rule line in the TZDB file.
 801      */
 802     final class TZDBRule extends TZDBMonthDayTime {
 803         /** The start year. */
 804         int startYear;
 805         /** The end year. */
 806         int endYear;
 807         /** The amount of savings. */
 808         int savingsAmount;




 210         System.out.println("   -dstdir <directory>   Where to output generated files (default srcdir)");
 211         System.out.println("   -version <version>    Specify the version, such as 2009a (optional)");
 212         System.out.println("   -help                 Print this usage message");
 213         System.out.println("   -verbose              Output verbose information during compilation");
 214         System.out.println(" There must be one directory for each version in srcdir");
 215         System.out.println(" Each directory must have the name of the version, such as 2009a");
 216         System.out.println(" Each directory must contain the unpacked tzdb files, such as asia or europe");
 217         System.out.println(" Directories must match the regex [12][0-9][0-9][0-9][A-Za-z0-9._-]+");
 218         System.out.println(" There will be one jar file for each version and one combined jar in dstdir");
 219         System.out.println(" If the version is specified, only that version is processed");
 220     }
 221 
 222     /**
 223      * Process to create the jar files.
 224      */
 225     private static void process(List<File> srcDirs, List<String> srcFileNames, File dstDir, String version, boolean verbose) {
 226         // build actual jar files
 227         Map<String, SortedMap<String, ZoneRules>> allBuiltZones = new TreeMap<>();
 228         Set<String> allRegionIds = new TreeSet<String>();
 229         Set<ZoneRules> allRules = new HashSet<ZoneRules>();
 230         Map<String, Map<String, String>> allLinks = new TreeMap<>();
 231 
 232         for (File srcDir : srcDirs) {
 233             // source files in this directory
 234             List<File> srcFiles = new ArrayList<>();
 235             for (String srcFileName : srcFileNames) {
 236                 File file = new File(srcDir, srcFileName);
 237                 if (file.exists()) {
 238                     srcFiles.add(file);
 239                 }
 240             }
 241             if (srcFiles.isEmpty()) {
 242                 continue;  // nothing to process
 243             }
 244 
 245             // compile
 246             String loopVersion = (srcDirs.size() == 1 && version != null)
 247                                  ? version : srcDir.getName();
 248             TzdbZoneRulesCompiler compiler = new TzdbZoneRulesCompiler(loopVersion, srcFiles, verbose);
 249             try {
 250                 // compile
 251                 compiler.compile();
 252                 SortedMap<String, ZoneRules> builtZones = compiler.getZones();
 253 
 254                 // output version-specific file
 255                 File dstFile = version == null ? new File(dstDir, "tzdb" + loopVersion + ".jar")
 256                                                : new File(dstDir, "tzdb.jar");
 257                 if (verbose) {
 258                     System.out.println("Outputting file: " + dstFile);
 259                 }
 260                 outputFile(dstFile, loopVersion, builtZones, compiler.links);
 261 
 262                 // create totals
 263                 allBuiltZones.put(loopVersion, builtZones);
 264                 allRegionIds.addAll(builtZones.keySet());
 265                 allRules.addAll(builtZones.values());
 266                 allLinks.put(loopVersion, compiler.links);
 267             } catch (Exception ex) {
 268                 System.out.println("Failed: " + ex.toString());
 269                 ex.printStackTrace();
 270                 System.exit(1);
 271             }
 272         }
 273 
 274         // output merged file
 275         if (version == null) {
 276             File dstFile = new File(dstDir, "tzdb-all.jar");
 277             if (verbose) {
 278                 System.out.println("Outputting combined file: " + dstFile);
 279             }
 280             outputFile(dstFile, allBuiltZones, allRegionIds, allRules, allLinks);
 281         }
 282     }
 283 
 284     /**
 285      * Outputs the file.
 286      */
 287     private static void outputFile(File dstFile,
 288                                    String version,
 289                                    SortedMap<String, ZoneRules> builtZones,
 290                                    Map<String, String> links) {
 291         Map<String, SortedMap<String, ZoneRules>> loopAllBuiltZones = new TreeMap<>();
 292         loopAllBuiltZones.put(version, builtZones);
 293         Set<String> loopAllRegionIds = new TreeSet<String>(builtZones.keySet());
 294         Set<ZoneRules> loopAllRules = new HashSet<ZoneRules>(builtZones.values());
 295         Map<String, Map<String, String>> loopAllLinks = new TreeMap<>();
 296         loopAllLinks.put(version, links);
 297         outputFile(dstFile, loopAllBuiltZones, loopAllRegionIds, loopAllRules, loopAllLinks);
 298     }
 299 
 300     /**
 301      * Outputs the file.
 302      */
 303     private static void outputFile(File dstFile,
 304                                    Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
 305                                    Set<String> allRegionIds,
 306                                    Set<ZoneRules> allRules,
 307                                    Map<String, Map<String, String>> allLinks) {
 308         try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(dstFile))) {
 309             outputTZEntry(jos, allBuiltZones, allRegionIds, allRules, allLinks);
 310         } catch (Exception ex) {
 311             System.out.println("Failed: " + ex.toString());
 312             ex.printStackTrace();
 313             System.exit(1);
 314         }
 315     }
 316 
 317     /**
 318      * Outputs the timezone entry in the JAR file.
 319      */
 320     private static void outputTZEntry(JarOutputStream jos,
 321                                       Map<String, SortedMap<String, ZoneRules>> allBuiltZones,
 322                                       Set<String> allRegionIds,
 323                                       Set<ZoneRules> allRules,
 324                                       Map<String, Map<String, String>> allLinks) {
 325         // this format is not publicly specified
 326         try {
 327             jos.putNextEntry(new ZipEntry("TZDB.dat"));
 328             DataOutputStream out = new DataOutputStream(jos);
 329 
 330             // file version
 331             out.writeByte(1);
 332             // group
 333             out.writeUTF("TZDB");
 334             // versions
 335             String[] versionArray = allBuiltZones.keySet().toArray(new String[allBuiltZones.size()]);
 336             out.writeShort(versionArray.length);
 337             for (String version : versionArray) {
 338                 out.writeUTF(version);
 339             }
 340             // regions
 341             String[] regionArray = allRegionIds.toArray(new String[allRegionIds.size()]);
 342             out.writeShort(regionArray.length);
 343             for (String regionId : regionArray) {
 344                 out.writeUTF(regionId);


 349             ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
 350             for (ZoneRules rules : rulesList) {
 351                 baos.reset();
 352                 DataOutputStream dataos = new DataOutputStream(baos);
 353                 rules.writeExternal(dataos);
 354                 dataos.close();
 355                 byte[] bytes = baos.toByteArray();
 356                 out.writeShort(bytes.length);
 357                 out.write(bytes);
 358             }
 359             // link version-region-rules
 360             for (String version : allBuiltZones.keySet()) {
 361                 out.writeShort(allBuiltZones.get(version).size());
 362                 for (Map.Entry<String, ZoneRules> entry : allBuiltZones.get(version).entrySet()) {
 363                      int regionIndex = Arrays.binarySearch(regionArray, entry.getKey());
 364                      int rulesIndex = rulesList.indexOf(entry.getValue());
 365                      out.writeShort(regionIndex);
 366                      out.writeShort(rulesIndex);
 367                 }
 368             }
 369             // alias-region
 370             for (String version : allLinks.keySet()) {
 371                 out.writeShort(allLinks.get(version).size());
 372                 for (Map.Entry<String, String> entry : allLinks.get(version).entrySet()) {
 373                      int aliasIndex = Arrays.binarySearch(regionArray, entry.getKey());
 374                      int regionIndex = Arrays.binarySearch(regionArray, entry.getValue());
 375                      out.writeShort(aliasIndex);
 376                      out.writeShort(regionIndex);
 377                 }
 378             }
 379             out.flush();
 380             jos.closeEntry();
 381         } catch (Exception ex) {
 382             System.out.println("Failed: " + ex.toString());
 383             ex.printStackTrace();
 384             System.exit(1);
 385         }
 386     }
 387 
 388     //-----------------------------------------------------------------------
 389     /** The TZDB rules. */
 390     private final Map<String, List<TZDBRule>> rules = new HashMap<>();
 391 
 392     /** The TZDB zones. */
 393     private final Map<String, List<TZDBZone>> zones = new HashMap<>();
 394     /** The TZDB links. */
 395 
 396     private final Map<String, String> links = new HashMap<>();
 397 
 398     /** The built zones. */


 621                 }
 622                 mdt.dayOfMonth = Integer.parseInt(dayRule);
 623             }
 624             if (st.hasMoreTokens()) {
 625                 String timeStr = st.nextToken();
 626                 int secsOfDay = parseSecs(timeStr);
 627                 if (secsOfDay == 86400) {
 628                     mdt.endOfDay = true;
 629                     secsOfDay = 0;
 630                 }
 631                 LocalTime time = LocalTime.ofSecondOfDay(secsOfDay);
 632                 mdt.time = time;
 633                 mdt.timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1));
 634             }
 635         }
 636     }
 637 
 638     private int parseYear(String str, int defaultYear) {
 639         if (YEAR.reset(str).matches()) {
 640             if (YEAR.group("min") != null) {
 641                 //return YEAR_MIN_VALUE;
 642                 return 1900;  // systemv has min
 643             } else if (YEAR.group("max") != null) {
 644                 return YEAR_MAX_VALUE;
 645             } else if (YEAR.group("only") != null) {
 646                 return defaultYear;
 647             }
 648             return Integer.parseInt(YEAR.group("year"));
 649         }
 650         throw new IllegalArgumentException("Unknown year: " + str);
 651     }
 652 
 653     private int parseMonth(String str) {
 654         if (MONTH.reset(str).matches()) {
 655             for (int moy = 1; moy < 13; moy++) {
 656                 if (MONTH.group(moy) != null) {
 657                     return moy;
 658                 }
 659             }
 660         }
 661         throw new IllegalArgumentException("Unknown month: " + str);
 662     }


 743             ZoneRulesBuilder bld = new ZoneRulesBuilder();
 744             for (TZDBZone tzdbZone : tzdbZones) {
 745                 bld = tzdbZone.addToBuilder(bld, rules);
 746             }
 747             ZoneRules buildRules = bld.toRules(zoneId);
 748             builtZones.put(zoneId, buildRules);
 749         }
 750 
 751         // build aliases
 752         for (String aliasId : links.keySet()) {
 753             String realId = links.get(aliasId);
 754             printVerbose("Linking alias " + aliasId + " to " + realId);
 755             ZoneRules realRules = builtZones.get(realId);
 756             if (realRules == null) {
 757                 realId = links.get(realId);  // try again (handle alias liked to alias)
 758                 printVerbose("Relinking alias " + aliasId + " to " + realId);
 759                 realRules = builtZones.get(realId);
 760                 if (realRules == null) {
 761                     throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId + "' for '" + version + "'");
 762                 }
 763                 links.put(aliasId, realId);
 764 
 765             }
 766             builtZones.put(aliasId, realRules);
 767         }
 768 
 769         // remove UTC and GMT
 770         //builtZones.remove("UTC");
 771         //builtZones.remove("GMT");
 772         //builtZones.remove("GMT0");
 773         builtZones.remove("GMT+0");
 774         builtZones.remove("GMT-0");
 775         links.remove("GMT+0");
 776         links.remove("GMT-0");
 777     }
 778 
 779     //-----------------------------------------------------------------------
 780     /**
 781      * Prints a verbose message.
 782      *
 783      * @param message  the message, not null
 784      */
 785     private void printVerbose(String message) {
 786         if (verbose) {
 787             System.out.println(message);
 788         }
 789     }
 790 
 791     //-----------------------------------------------------------------------
 792     /**
 793      * Class representing a month-day-time in the TZDB file.
 794      */
 795     abstract class TZDBMonthDayTime {
 796         /** The month of the cutover. */
 797         int month = 1;
 798         /** The day-of-month of the cutover. */
 799         int dayOfMonth = 1;
 800         /** Whether to adjust forwards. */
 801         boolean adjustForwards = true;
 802         /** The day-of-week of the cutover. */
 803         int dayOfWeek = -1;
 804         /** The time of the cutover. */
 805         LocalTime time = LocalTime.MIDNIGHT;
 806         /** Whether this is midnight end of day. */
 807         boolean endOfDay;
 808         /** The time of the cutover. */
 809         TimeDefinition timeDefinition = TimeDefinition.WALL;

 810         void adjustToFowards(int year) {
 811             if (adjustForwards == false && dayOfMonth > 0) {
 812                 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6);
 813                 dayOfMonth = adjustedDate.getDayOfMonth();
 814                 month = adjustedDate.getMonth();
 815                 adjustForwards = true;
 816             }
 817         }
 818     }
 819 
 820     /**
 821      * Class representing a rule line in the TZDB file.
 822      */
 823     final class TZDBRule extends TZDBMonthDayTime {
 824         /** The start year. */
 825         int startYear;
 826         /** The end year. */
 827         int endYear;
 828         /** The amount of savings. */
 829         int savingsAmount;