OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 180 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2011-2019 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 http://www.gnu.org/licenses/gpl-3.0.html 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.style.data;

import org.openconcerto.openoffice.ODPackage;
import org.openconcerto.openoffice.ODValueType;
import org.openconcerto.openoffice.StyleProperties;
import org.openconcerto.openoffice.XMLVersion;
import org.openconcerto.openoffice.spreadsheet.CellStyle;
import org.openconcerto.utils.CollectionUtils;
import org.openconcerto.utils.convertor.NumberConvertor;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Consumer;

import org.jdom.Attribute;
import org.jdom.Element;
import org.jdom.Namespace;

import com.ibm.icu.text.DateTimePatternGenerator;

// from section 16.27.10 in v1.2-cs01-part1
public class DateStyle extends DataStyle {

    private static final String AUTOMATIC_ORDER_ATTRNAME = "automatic-order";

    private static final String FORMAT_SOURCE_ATTRNAME = "format-source";
    private static final String FORMAT_SOURCE_FIXED = "fixed";
    private static final String FORMAT_SOURCE_LANG = "language";

    // see http://download.oracle.com/javase/6/docs/technotes/guides/intl/calendar.doc.html
    private static final Calendar BUDDHIST_CAL = Calendar.getInstance(new Locale("th", "TH"));
    private static final Calendar JAPANESE_CAL = Calendar.getInstance(new Locale("ja", "JP", "JP"));
    private static final Calendar GREGORIAN_CAL = new GregorianCalendar();

    static final DataStyleDesc<DateStyle> DESC = new DataStyleDesc<DateStyle>(DateStyle.class, XMLVersion.OD, "date-style", "N") {
        @Override
        public DateStyle create(ODPackage pkg, Element e) {
            return new DateStyle(pkg, e);
        }
    };

    static final boolean isShort(final Element elem) {
        // in OOo the default is short
        return !"long".equals(elem.getAttributeValue("style", elem.getNamespace("number")));
    }

    // with LO 6.4 only year,month,day,day-of-week have variable length
    static final boolean isShort(final Element elem, final boolean fixed, final Locale locale) {
        if (fixed)
            return isShort(elem);
        else
            return Locale.US.equals(locale);
    }

    private static final Calendar getCalendar(final Element elem, Calendar defaultCal) {
        final Calendar res;
        final String cal = elem.getAttributeValue("calendar", elem.getNamespace());
        if (cal == null) {
            res = defaultCal;
        } else if ("buddhist".equals(cal)) {
            res = BUDDHIST_CAL;
        } else if ("gengou".equals(cal)) {
            res = JAPANESE_CAL;
        } else if ("gregorian".equals(cal)) {
            res = GREGORIAN_CAL;
        } else {
            throw new IllegalArgumentException("Unsupported calendar : " + cal);
        }
        return res;
    }

    static String formatSecondFraction(final Locale styleLocale, final BigDecimal seconds, final int decPlaces) {
        if (decPlaces > 0) {
            final DecimalFormat decFormat = new DecimalFormat();
            decFormat.setDecimalFormatSymbols(new DecimalFormatSymbols(styleLocale));
            decFormat.setMinimumIntegerDigits(0);
            decFormat.setMaximumIntegerDigits(0);
            decFormat.setMinimumFractionDigits(decPlaces);
            decFormat.setMaximumFractionDigits(decPlaces);
            // .12 or .578
            return decFormat.format(seconds);
        } else {
            return "";
        }
    }

    public DateStyle(final ODPackage pkg, Element elem) {
        super(pkg, elem, ODValueType.DATE);
    }

    @Override
    protected Object convertNonNull(Object o) {
        if (o instanceof Number)
            return getEpoch().getDate(NumberConvertor.toBigDecimal((Number) o));
        else
            return null;
    }

    public final boolean isFormatSourceFixed() {
        return this.getElement().getAttributeValue(FORMAT_SOURCE_ATTRNAME, this.getElement().getNamespace(), FORMAT_SOURCE_FIXED).equals(FORMAT_SOURCE_FIXED);
    }

    final void setFormatSourceFixed(final boolean b) {
        if (b) {
            this.getElement().removeAttribute(FORMAT_SOURCE_ATTRNAME, this.getElement().getNamespace());
        } else {
            this.getElement().setAttribute(FORMAT_SOURCE_ATTRNAME, FORMAT_SOURCE_LANG, this.getElement().getNamespace());
        }
    }

    public final boolean isAutomaticOrder() {
        return StyleProperties.parseBoolean(this.getElement().getAttributeValue(AUTOMATIC_ORDER_ATTRNAME, this.getElement().getNamespace()), false);
    }

    final void setAutomaticOrder(final boolean b) {
        if (b) {
            this.getElement().setAttribute(AUTOMATIC_ORDER_ATTRNAME, Boolean.toString(b), this.getElement().getNamespace());
        } else {
            this.getElement().removeAttribute(AUTOMATIC_ORDER_ATTRNAME, this.getElement().getNamespace());
        }
    }

    private final void format(final StringBuilder res, final List<String> skeleton, final StringBuilder pattern, final Locale styleLocale, final boolean automaticOrder, final Calendar currentCalendar,
            final Date d) {
        if (pattern.length() > 0) {
            if (automaticOrder) {
                // ask ICU the preferred order for our skeleton,
                // then change order of components in our pattern (thus keeping literals)

                // e.g. [EEEE, dd, MM, yyyy] => "EEEE, MM dd yyyy"
                final String bestPattern = DateTimePatternGenerator.getInstance(styleLocale).getBestPattern(CollectionUtils.join(skeleton, ""), DateTimePatternGenerator.MATCH_ALL_FIELDS_LENGTH);
                final SortedMap<Integer, String> bestOrder = new TreeMap<>();
                for (final String comp : skeleton) {
                    final int indexOf = bestPattern.indexOf(comp);
                    if (indexOf < 0)
                        throw new IllegalStateException("Missing " + comp + " in best pattern : " + bestPattern);
                    final String prev = bestOrder.put(indexOf, comp);
                    if (prev != null)
                        throw new IllegalStateException("Duplicate " + comp + " in best pattern : " + bestPattern);
                }
                assert bestOrder.size() == skeleton.size();
                // e.g. [EEEE, MM, dd, yyyy]
                final List<String> reorderedSkeleton = new ArrayList<>(bestOrder.values());

                int indexOfStart = 0;
                for (int i = 0; i < skeleton.size(); i++) {
                    // e.g. "dd"
                    final String comp = skeleton.get(i);
                    final int reorderedIndex = reorderedSkeleton.indexOf(comp);
                    assert reorderedIndex >= 0;
                    if (i != reorderedIndex) {
                        // e.g. "MM"
                        final String compToSwap = reorderedSkeleton.get(i);

                        // replace "dd" by "MM"
                        final int indexOfComp = pattern.indexOf(comp, indexOfStart);
                        pattern.replace(indexOfComp, indexOfComp + comp.length(), compToSwap);
                        // avoid swapping back
                        indexOfStart = indexOfComp + compToSwap.length();
                    }
                }
            }
            final SimpleDateFormat fmt = new SimpleDateFormat(pattern.toString(), styleLocale);
            pattern.setLength(0);
            fmt.setCalendar((Calendar) currentCalendar.clone());
            res.append(fmt.format(d));
        }
    }

    @Override
    public String format(Object o, CellStyle defaultStyle, boolean lenient) {
        final Date d = o instanceof Calendar ? ((Calendar) o).getTime() : (Date) o;
        final Namespace numberNS = this.getElement().getNamespace();
        final boolean automaticOrder = this.isAutomaticOrder();
        final boolean fixedLength = this.isFormatSourceFixed();
        final Locale styleLocale = this.getLocale();
        final Calendar styleCalendar = Calendar.getInstance(styleLocale);
        final StringBuilder res = new StringBuilder();

        Calendar currentCalendar = styleCalendar;
        final List<String> skeleton = automaticOrder ? new ArrayList<>() : null;
        final StringBuilder sb = new StringBuilder();

        final Consumer<String> addComp = (s) -> {
            sb.append(s);
            if (skeleton != null)
                skeleton.add(s);
        };

        @SuppressWarnings("unchecked")
        final List<Element> children = this.getElement().getChildren();
        for (final Element elem : children) {
            if (elem.getNamespace().equals(numberNS)) {
                final Calendar calendarLocaleElem = getCalendar(elem, styleCalendar);
                if (!calendarLocaleElem.equals(currentCalendar)) {
                    format(res, skeleton, sb, styleLocale, automaticOrder, currentCalendar, d);
                    currentCalendar = calendarLocaleElem;
                }

                if (elem.getName().equals("text")) {
                    DataStyle.addStringLiteral(sb, elem.getText());
                } else if (elem.getName().equals("era")) {
                    addComp.accept(isShort(elem) ? "G" : "GGGG");
                } else if (elem.getName().equals("year")) {
                    addComp.accept(isShort(elem, fixedLength, styleLocale) ? "yy" : "yyyy");
                } else if (elem.getName().equals("quarter")) {
                    final Calendar cal = (Calendar) currentCalendar.clone();
                    cal.setTime(d);
                    final double quarterLength = cal.getActualMaximum(Calendar.MONTH) / 4.0;
                    final int quarter = (int) (cal.get(Calendar.MONTH) / quarterLength + 1);
                    assert quarter >= 1 && quarter <= 4;
                    // TODO localize and honor short/long style
                    reportError("Quarters are not localized", lenient);
                    DataStyle.addStringLiteral(sb, isShort(elem) ? "Q" + quarter : "Q" + quarter);
                } else if (elem.getName().equals("month")) {
                    final Attribute possessive = elem.getAttribute("possessive-form", numberNS);
                    if (possessive != null)
                        reportError("Ignoring " + possessive, lenient);
                    if (!StyleProperties.parseBoolean(elem.getAttributeValue("textual", numberNS), false))
                        addComp.accept(isShort(elem, fixedLength, styleLocale) ? "M" : "MM");
                    else
                        addComp.accept(isShort(elem) ? "MMM" : "MMMM");
                } else if (elem.getName().equals("week-of-year")) {
                    addComp.accept("w");
                } else if (elem.getName().equals("day")) {
                    addComp.accept(isShort(elem, fixedLength, styleLocale) ? "d" : "dd");
                } else if (elem.getName().equals("day-of-week")) {
                    addComp.accept(isShort(elem, fixedLength, styleLocale) ? "E" : "EEEE");
                } else if (elem.getName().equals("am-pm")) {
                    addComp.accept("a");
                } else if (elem.getName().equals("hours")) {
                    // see 16.27.22 : If a <number:am-pm> element is contained in a date or time
                    // style, hours are displayed using values from 1 to 12 only.
                    if (getElement().getChild("am-pm", numberNS) == null)
                        addComp.accept(isShort(elem) ? "H" : "HH");
                    else
                        addComp.accept(isShort(elem) ? "h" : "hh");
                } else if (elem.getName().equals("minutes")) {
                    addComp.accept(isShort(elem) ? "m" : "mm");
                } else if (elem.getName().equals("seconds")) {
                    addComp.accept(isShort(elem) ? "s" : "ss");
                    final int decPlaces = StyleProperties.parseInt(elem.getAttributeValue("decimal-places", numberNS), 0);
                    if (decPlaces > 0) {
                        // use styleLocale since <seconds> hasn't @calendar
                        final Calendar cal = Calendar.getInstance(styleLocale);
                        cal.setTime(d);
                        final BigDecimal secondFractions = new BigDecimal(cal.get(Calendar.MILLISECOND)).movePointLeft(3);
                        assert secondFractions.compareTo(BigDecimal.ONE) < 0;
                        final String fractionPart = formatSecondFraction(styleLocale, secondFractions, decPlaces);
                        DataStyle.addStringLiteral(sb, fractionPart);
                    }
                }
            }
        }
        format(res, skeleton, sb, styleLocale, automaticOrder, currentCalendar, d);
        return res.toString();
    }
}