OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

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