Dépôt officiel du code source de l'ERP OpenConcerto



Rev 80 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
 * copy of the License at See the License for the specific
 * language governing permissions and limitations under the License.
 * When distributing the software, include this License Header Notice in each file.
 package org.openconcerto.openoffice;

import org.openconcerto.utils.TimeUtils;
import org.openconcerto.utils.cache.LRUMap;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.TimeZone;

import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.Duration;

import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.Immutable;

 * The null date of an OpenDocument.
public final class ODEpoch {

    static private final BigDecimal MS_PER_DAY = BigDecimal.valueOf(24l * 60l * 60l * 1000l);
    static private final DateFormat DATE_FORMAT;
    static private final ODEpoch DEFAULT_EPOCH;
    static private final CachedTransformer<String, ODEpoch, ParseException> cache = new CachedTransformer<>(new LRUMap<>(16, 4), ODEpoch::new);

    static {
        DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
        try {
            DEFAULT_EPOCH = new ODEpoch("1899-12-30");
        } catch (ParseException e) {
            // shouldn't happen since string are constants
            throw new IllegalStateException(e);

    static public final ODEpoch getDefaultEpoch() {
        return DEFAULT_EPOCH;

    static public final ODEpoch getInstance(String date) throws ParseException {
        if (date == null || date.equals(DEFAULT_EPOCH.getDateString())) {
            return DEFAULT_EPOCH;
        } else {
            synchronized (cache) {
                return cache.transformChecked(date);

    public static final BigDecimal getDays(final java.time.Duration d) {
        return getDays(d.toMillis());

    public static final BigDecimal getDays(final long millis) {
        return BigDecimal.valueOf(millis).divide(MS_PER_DAY, MathContext.DECIMAL128);

    static private final Calendar parse(final String date) throws ParseException {
        synchronized (DATE_FORMAT) {
            final Calendar cal = (Calendar) DATE_FORMAT.getCalendar().clone();
            return cal;

    private final String dateString;
    private final Calendar epochUTC;

    private ODEpoch(final String date) throws ParseException {
        this.dateString = date;
        this.epochUTC = parse(date);
        assert this.epochUTC.getTimeZone().equals(DATE_FORMAT.getTimeZone());

    public final String getDateString() {
        return this.dateString;

    public final Calendar getCalendar() {
        return (Calendar) this.epochUTC.clone();

    private final Calendar getDate(final Duration duration) {
        // If we don't use the UTC calendar, we go from 0:00 (the epoch), we add n days, we get
        // to the last Sunday of March at 0:00, so far so good, but then we add say 10 hours, thus
        // going through the change of offset, and arriving at 11:00.
        final Calendar res = getCalendar();
        return res;

    public final Duration normalizeToDays(final Duration dur) {
        // we have to convert to a date to know the duration of years and months
        final Duration res = dur.getYears() == 0 && dur.getMonths() == 0 ? dur : getDuration(getDays(dur));
        assert res.getYears() == 0 && res.getMonths() == 0;
        return res;

    public final Duration normalizeToHours(final Duration dur) {
        final Duration durationInDays = normalizeToDays(dur);
        final BigInteger days = (BigInteger) durationInDays.getField(DatatypeConstants.DAYS);
        if (days == null || days.equals(BigInteger.ZERO))
            return durationInDays;
        final BigInteger hours = ((BigInteger) durationInDays.getField(DatatypeConstants.HOURS)).add(days.multiply(BigInteger.valueOf(24)));
        return TimeUtils.getTypeFactory().newDuration(days.signum() >= 0, BigInteger.ZERO, BigInteger.ZERO, BigInteger.ZERO, hours, (BigInteger) durationInDays.getField(DatatypeConstants.MINUTES),
                (BigDecimal) durationInDays.getField(DatatypeConstants.SECONDS));

    public final BigDecimal getDays(final Duration duration) {
        return getDays(getDate(duration));

    public final BigDecimal getDays(final Calendar cal) {
        // can't use Duration.normalizeWith() since it doesn't handle DST, i.e. going from winter to
        // summer at midnight will miss a day
        final long diff = TimeUtils.normalizeLocalTime(cal) - this.epochUTC.getTimeInMillis();
        return getDays(diff);

    public final Calendar getDate(final BigDecimal days) {
        return getDate(days, ODValueType.getCalendar());

    public final Calendar getDate(final BigDecimal days, final Calendar res) {
        final Calendar utcCal = getDate(getDuration(days));
        // can't use getTimeZone().getOffset() since we have no idea for the UTC time
        return TimeUtils.copyLocalTime(utcCal, res);

    private final static Duration getDuration(final BigDecimal days) {
        final BigDecimal posDays = days.abs();
        final BigInteger wholeDays = posDays.toBigInteger().abs();
        final BigDecimal hours = posDays.subtract(new BigDecimal(wholeDays)).multiply(BigDecimal.valueOf(24));
        final BigInteger wholeHours = hours.toBigInteger();
        final BigDecimal minutes = hours.subtract(new BigDecimal(wholeHours)).multiply(BigDecimal.valueOf(60));
        final BigInteger wholeMinutes = minutes.toBigInteger();
        // round to 16 digits, i.e. 10^-14 seconds is more than enough
        // it is required since the number coming from getDays() might have been rounded
        final BigDecimal seconds = minutes.subtract(new BigDecimal(wholeMinutes)).multiply(BigDecimal.valueOf(60)).round(MathContext.DECIMAL64);
        return TimeUtils.getTypeFactory().newDuration(days.signum() >= 0, BigInteger.ZERO, BigInteger.ZERO, wholeDays, wholeHours, wholeMinutes, seconds);