OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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

Rev Author Line No. Line
20 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.openoffice.style.data;
15
 
16
import org.openconcerto.openoffice.Log;
25 ilm 17
import org.openconcerto.openoffice.ODEpoch;
20 ilm 18
import org.openconcerto.openoffice.ODPackage;
25 ilm 19
import org.openconcerto.openoffice.ODValueType;
180 ilm 20
import org.openconcerto.openoffice.OOUtils;
20 ilm 21
import org.openconcerto.openoffice.Style;
22
import org.openconcerto.openoffice.StyleDesc;
23
import org.openconcerto.openoffice.StyleProperties;
24
import org.openconcerto.openoffice.XMLVersion;
25
import org.openconcerto.openoffice.spreadsheet.CellStyle;
26
import org.openconcerto.openoffice.text.TextStyle.StyleTextProperties;
27
import org.openconcerto.utils.NumberUtils;
28
 
29
import java.math.RoundingMode;
30
import java.text.DecimalFormat;
174 ilm 31
import java.text.DecimalFormatSymbols;
20 ilm 32
import java.util.Arrays;
33
import java.util.Collections;
73 ilm 34
import java.util.HashSet;
20 ilm 35
import java.util.List;
174 ilm 36
import java.util.Locale;
20 ilm 37
import java.util.Map.Entry;
73 ilm 38
import java.util.Set;
20 ilm 39
import java.util.SortedMap;
40
import java.util.TreeMap;
41
import java.util.regex.Matcher;
42
import java.util.regex.Pattern;
43
 
44
import org.jdom.Attribute;
45
import org.jdom.Element;
46
import org.jdom.Namespace;
47
 
48
// from section 16.27 in v1.2-cs01-part1
49
public abstract class DataStyle extends Style {
50
    private static final int DEFAULT_GROUPING_SIZE = new DecimalFormat().getGroupingSize();
174 ilm 51
    // 15 as of LibreOffice 6, was 10 earlier
52
    /**
53
     * The default number of decimal digits if neither defined in the style nor in default-style.
54
     */
180 ilm 55
    public static final int DEFAULT_DECIMAL_PLACES;
20 ilm 56
    private static final Pattern QUOTE_PATRN = Pattern.compile("'", Pattern.LITERAL);
57
    private static final Pattern EXP_PATTERN = Pattern.compile("E(\\d+)$");
58
 
59
    public static int getDecimalPlaces(final CellStyle defaultStyle) {
73 ilm 60
        if (defaultStyle != null) {
180 ilm 61
            final int res = defaultStyle.getTableCellProperties(null).getDecimalPlaces();
62
            // Ignore invalid value
63
            return res < 0 ? DEFAULT_DECIMAL_PLACES : res;
73 ilm 64
        } else {
65
            return DEFAULT_DECIMAL_PLACES;
66
        }
20 ilm 67
    }
68
 
180 ilm 69
    protected static final int parsePositive(final String attr, final boolean lenient) {
70
        final int res = Integer.parseInt(attr);
71
        if (res < 0) {
72
            reportError("Negative value for " + attr, lenient);
73
            return 0;
74
        }
75
        return res;
76
    }
77
 
20 ilm 78
    public static void addStringLiteral(final StringBuilder formatSB, final String s) {
79
        formatSB.append('\'');
80
        formatSB.append(QUOTE_PATRN.matcher(s).replaceAll("''"));
81
        formatSB.append('\'');
82
    }
83
 
73 ilm 84
    public static final Set<Class<? extends DataStyle>> DATA_STYLES;
65 ilm 85
    private static final DataStyleDesc<?>[] DATA_STYLES_DESCS = new DataStyleDesc<?>[] { NumberStyle.DESC, PercentStyle.DESC, TextStyle.DESC, CurrencyStyle.DESC, DateStyle.DESC, TimeStyle.DESC,
20 ilm 86
            BooleanStyle.DESC };
73 ilm 87
    static {
88
        final Set<Class<? extends DataStyle>> l = new HashSet<Class<? extends DataStyle>>(DATA_STYLES_DESCS.length);
89
        l.add(NumberStyle.class);
90
        l.add(PercentStyle.class);
91
        l.add(TextStyle.class);
92
        l.add(CurrencyStyle.class);
93
        l.add(DateStyle.class);
94
        l.add(TimeStyle.class);
95
        l.add(BooleanStyle.class);
96
        DATA_STYLES = Collections.unmodifiableSet(l);
97
        assert DATA_STYLES_DESCS.length == DATA_STYLES.size() : "Discrepancy between classes and descs";
180 ilm 98
 
99
        final String decPlacesProp = System.getProperty("openDocument.defaultDecimalPlaces");
100
        final int decPlacesParsed = decPlacesProp == null ? -1 : Integer.parseInt(decPlacesProp);
101
        // Ignore invalid value
102
        DEFAULT_DECIMAL_PLACES = decPlacesParsed < 0 ? 15 : decPlacesParsed;
103
        assert DEFAULT_DECIMAL_PLACES >= 0;
73 ilm 104
    }
20 ilm 105
 
106
    public static abstract class DataStyleDesc<S extends DataStyle> extends StyleDesc<S> {
107
 
108
        protected DataStyleDesc(Class<S> clazz, XMLVersion version, String elemName, String baseName) {
109
            super(clazz, version, elemName, baseName);
110
            this.setElementNS(getVersion().getNS("number"));
111
            // from 19.469 in v1.2-cs01-part1
174 ilm 112
            this.getRefElementsMap().addAll("style:data-style-name",
20 ilm 113
                    Arrays.asList("presentation:date-time-decl", "style:style", "text:creation-date", "text:creation-time", "text:database-display", "text:date", "text:editing-duration",
114
                            "text:expression", "text:meta-field", "text:modification-date", "text:modification-time", "text:print-date", "text:print-time", "text:table-formula", "text:time",
115
                            "text:user-defined", "text:user-field-get", "text:user-field-input", "text:variable-get", "text:variable-input", "text:variable-set"));
80 ilm 116
            this.getRefElementsMap().add("style:apply-style-name", "style:map");
20 ilm 117
        }
118
    }
119
 
65 ilm 120
    static public void registerDesc() {
121
        for (final StyleDesc<?> d : DATA_STYLES_DESCS)
122
            Style.registerAllVersions(d);
123
    }
124
 
125
    static public <S extends DataStyle> DataStyleDesc<S> getDesc(final Class<S> clazz, final XMLVersion version) {
126
        return (DataStyleDesc<S>) Style.getStyleDesc(clazz, version);
127
    }
128
 
25 ilm 129
    private final ODValueType type;
20 ilm 130
    private StyleTextProperties textProps;
131
 
25 ilm 132
    protected DataStyle(final ODPackage pkg, Element elem, final ODValueType type) {
20 ilm 133
        super(pkg, elem);
134
        this.type = type;
135
    }
136
 
25 ilm 137
    public final ODValueType getDataType() {
20 ilm 138
        return this.type;
139
    }
140
 
25 ilm 141
    public final ODEpoch getEpoch() {
142
        return this.getPackage().getODDocument().getEpoch();
143
    }
144
 
145
    /**
146
     * Convert the passed object to something that {@link #format(Object, CellStyle, boolean)} can
147
     * accept.
148
     *
149
     * @param o the object to convert.
150
     * @return an object that can be formatted, <code>null</code> if <code>o</code> cannot be
151
     *         converted.
152
     * @throws NullPointerException if <code>o</code> is <code>null</code>.
153
     * @see #canFormat(Class)
154
     */
155
    public final Object convert(final Object o) throws NullPointerException {
156
        if (o == null)
157
            throw new NullPointerException();
158
 
159
        final Object res;
160
        if (this.canFormat(o.getClass()))
161
            res = o;
162
        else
163
            res = this.convertNonNull(o);
164
        assert res == null || this.canFormat(res.getClass());
165
        return res;
166
    }
167
 
168
    // o is not null and canFormat(o.getClass()) is false
169
    // return null if o cannot be converted
170
    protected abstract Object convertNonNull(Object o);
171
 
172
    /**
173
     * Whether instances of the passed class can be {@link #format(Object, CellStyle, boolean)
174
     * formatted}.
175
     *
176
     * @param toFormat the class.
177
     * @return <code>true</code> if instances of <code>toFormat</code> can be formatted.
178
     */
20 ilm 179
    public final boolean canFormat(Class<?> toFormat) {
25 ilm 180
        return this.getDataType().canFormat(toFormat);
20 ilm 181
    }
182
 
183
    public final String getTitle() {
184
        return this.getElement().getAttributeValue("title", getElement().getNamespace());
185
    }
186
 
187
    public final StyleTextProperties getTextProperties() {
188
        if (this.textProps == null)
189
            this.textProps = new StyleTextProperties(this);
190
        return this.textProps;
191
    }
192
 
193
    public abstract String format(final Object o, final CellStyle defaultStyle, boolean lenient) throws UnsupportedOperationException;
194
 
61 ilm 195
    static protected final void reportError(String msg, boolean lenient) throws UnsupportedOperationException {
20 ilm 196
        if (lenient)
197
            Log.get().warning(msg);
198
        else
199
            throw new UnsupportedOperationException(msg);
200
    }
201
 
174 ilm 202
    public final Locale getLocale() {
180 ilm 203
        return this.getLocale(false);
174 ilm 204
    }
205
 
180 ilm 206
    public final Locale getLocale(final boolean local) {
207
        return this.getLocale(this.getElement(), local);
174 ilm 208
    }
209
 
180 ilm 210
    protected final Locale getLocale(final Element elem, final boolean local) {
211
        final Locale res = OOUtils.getElementLocale(elem);
212
        return local || res != null ? res : this.getPackage().getLocale();
20 ilm 213
    }
214
 
180 ilm 215
    public final void setLocale(final Locale l) {
216
        OOUtils.setElementLocale(this.getElement(), this.getElement().getNamespace(), l);
217
    }
218
 
219
    @SuppressWarnings("unchecked")
220
    public final List<Element> getMapChildren() {
221
        return this.getElement().getChildren("map", getSTYLE());
222
    }
223
 
224
    protected final String formatNumberOrScientificNumber(final Element elem, final Number n, CellStyle defaultStyle, final boolean lenient) {
225
        return this.formatNumberOrScientificNumber(elem, n, 1, defaultStyle, lenient);
226
    }
227
 
228
    protected final String formatNumberOrScientificNumber(final Element elem, final Number n, final int multiplier, CellStyle defaultStyle, final boolean lenient) {
20 ilm 229
        final Namespace numberNS = this.getElement().getNamespace();
230
        final StringBuilder numberSB = new StringBuilder();
231
 
232
        final List<?> embeddedTexts = elem.getChildren("embedded-text", numberNS);
233
        final SortedMap<Integer, String> embeddedTextByPosition = new TreeMap<Integer, String>(Collections.reverseOrder());
234
        for (final Object o : embeddedTexts) {
235
            final Element embeddedText = (Element) o;
236
            embeddedTextByPosition.put(Integer.valueOf(embeddedText.getAttributeValue("position", numberNS)), embeddedText.getText());
237
        }
238
 
239
        final Attribute factorAttr = elem.getAttribute("display-factor", numberNS);
240
        final double factor = (factorAttr != null ? Double.valueOf(factorAttr.getValue()) : 1) / multiplier;
241
 
242
        // default value from 19.348
243
        final boolean grouping = StyleProperties.parseBoolean(elem.getAttributeValue("grouping", numberNS), false);
244
 
245
        final String minIntDigitsAttr = elem.getAttributeValue("min-integer-digits", numberNS);
246
        final int minIntDig = minIntDigitsAttr == null ? 0 : Integer.parseInt(minIntDigitsAttr);
247
        if (minIntDig == 0) {
248
            numberSB.append('#');
249
        } else {
250
            for (int i = 0; i < minIntDig; i++)
251
                numberSB.append('0');
252
        }
253
 
254
        // e.g. if it's "--", 12,3 is displayed "12,3" and 12 is displayed "12,--"
180 ilm 255
        // From v1.3 §19.356.2 decimal-replacement can be empty
256
        final String decReplacement = elem.getAttributeValue("decimal-replacement", numberNS, "");
20 ilm 257
        final boolean decSeparatorAlwaysShown;
180 ilm 258
        if (!decReplacement.isEmpty() && !NumberUtils.hasFractionalPart(n)) {
20 ilm 259
            decSeparatorAlwaysShown = true;
260
            numberSB.append('.');
261
            // escape quote in replacement
262
            addStringLiteral(numberSB, decReplacement);
263
        } else {
264
            decSeparatorAlwaysShown = false;
265
            // see 19.343.2
180 ilm 266
            final String decPlacesAttr = elem.getAttributeValue("decimal-places", numberNS, "");
267
            final String minDecPlacesAttr = elem.getAttributeValue("min-decimal-places", numberNS, "");
268
            final int forcedPlaces, nonZeroPlaces;
269
            if (!decPlacesAttr.isEmpty()) {
270
                final int decPlaces = parsePositive(decPlacesAttr, lenient);
271
                if (minDecPlacesAttr.isEmpty()) {
272
                    forcedPlaces = decPlaces;
273
                    nonZeroPlaces = 0;
274
                } else {
275
                    forcedPlaces = parsePositive(minDecPlacesAttr, lenient);
276
                    if (forcedPlaces > decPlaces) {
277
                        DataStyle.reportError("min-decimal-places greater than decimal-places : " + minDecPlacesAttr + " > " + decPlacesAttr, lenient);
278
                        nonZeroPlaces = 0;
279
                    } else {
280
                        nonZeroPlaces = decPlaces - forcedPlaces;
281
                    }
282
                }
73 ilm 283
            } else {
284
                // default style specifies the maximum
180 ilm 285
                forcedPlaces = 0;
286
                nonZeroPlaces = getDecimalPlaces(defaultStyle);
73 ilm 287
            }
20 ilm 288
 
180 ilm 289
            if (forcedPlaces + nonZeroPlaces > 0) {
20 ilm 290
                numberSB.append('.');
180 ilm 291
                for (int i = 0; i < forcedPlaces; i++)
292
                    numberSB.append('0');
293
                for (int i = 0; i < nonZeroPlaces; i++)
294
                    numberSB.append('#');
20 ilm 295
            }
296
        }
297
 
298
        final Attribute minExpAttr = elem.getAttribute("min-exponent-digits", numberNS);
180 ilm 299
        final boolean forcedExpSign;
20 ilm 300
        if (minExpAttr != null) {
180 ilm 301
            forcedExpSign = Boolean.parseBoolean(elem.getAttributeValue("forced-exponent-sign", numberNS, "true"));
20 ilm 302
            numberSB.append('E');
303
            for (int i = 0; i < Integer.parseInt(minExpAttr.getValue()); i++)
304
                numberSB.append('0');
180 ilm 305
        } else {
306
            forcedExpSign = false;
20 ilm 307
        }
308
 
174 ilm 309
        final DecimalFormatSymbols symbols = new DecimalFormatSymbols(this.getLocale());
310
 
311
        final DecimalFormat decFormat = new DecimalFormat(numberSB.toString(), symbols);
20 ilm 312
        // Java always use HALF_EVEN
313
        decFormat.setRoundingMode(RoundingMode.HALF_UP);
314
        decFormat.setGroupingUsed(grouping);
315
        // needed since the default size is overwritten by the pattern
316
        decFormat.setGroupingSize(DEFAULT_GROUPING_SIZE);
317
        decFormat.setDecimalSeparatorAlwaysShown(decSeparatorAlwaysShown);
318
        String res = decFormat.format(NumberUtils.divide(n, factor));
180 ilm 319
        // There's no way to force the plus sign in DecimalFormat
320
        if (forcedExpSign) {
20 ilm 321
            final Matcher m = EXP_PATTERN.matcher(res);
322
            if (m.find())
323
                res = res.substring(0, m.start()) + "E+" + m.group(1);
324
        }
325
        if (embeddedTextByPosition.size() > 0) {
326
            final int intDigits = Math.max(minIntDig, NumberUtils.intDigits(n));
327
            // each time we insert text the decimal point moves
328
            int offset = 0;
329
            // sorted descending to avoid overwriting
330
            for (Entry<Integer, String> e : embeddedTextByPosition.entrySet()) {
331
                final String embeddedText = e.getValue();
332
                // the text will be before this index
333
                final int index = Math.max(0, offset + intDigits - e.getKey().intValue());
334
                res = res.substring(0, index) + embeddedText + res.substring(index);
335
                offset += embeddedText.length();
336
            }
337
        }
338
        return res;
339
    }
340
}