OceanusDataFormatter.java

/*******************************************************************************
 * Oceanus: Java Utilities
 * Copyright 2012,2025 Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package net.sourceforge.joceanus.oceanus.format;

import net.sourceforge.joceanus.oceanus.base.OceanusException;
import net.sourceforge.joceanus.oceanus.convert.OceanusDataConverter;
import net.sourceforge.joceanus.oceanus.date.OceanusDate;
import net.sourceforge.joceanus.oceanus.date.OceanusDateFormatter;
import net.sourceforge.joceanus.oceanus.date.OceanusDateRange;
import net.sourceforge.joceanus.oceanus.decimal.OceanusDecimal;
import net.sourceforge.joceanus.oceanus.decimal.OceanusDecimalFormatter;
import net.sourceforge.joceanus.oceanus.decimal.OceanusDecimalParser;
import net.sourceforge.joceanus.oceanus.decimal.OceanusMoney;
import net.sourceforge.joceanus.oceanus.decimal.OceanusPrice;
import net.sourceforge.joceanus.oceanus.decimal.OceanusRate;
import net.sourceforge.joceanus.oceanus.decimal.OceanusRatio;
import net.sourceforge.joceanus.oceanus.decimal.OceanusUnits;
import net.sourceforge.joceanus.oceanus.profile.OceanusProfile;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * Data Formatter.
 */
public class OceanusDataFormatter {
    /**
     * Formatter extension.
     */
    public interface OceanusDataFormatterExtension {
        /**
         * Format an object value.
         * @param pValue the object to format
         * @return the formatted value (or null if not recognised)
         */
        String formatObject(Object pValue);
    }

    /**
     * Invalid class error.
     */
    private static final String ERROR_CLASS = "Invalid Class: ";

    /**
     * Date Formatter.
     */
    private final OceanusDateFormatter theDateFormatter;

    /**
     * Decimal Formatter.
     */
    private final OceanusDecimalFormatter theDecimalFormatter;

    /**
     * Date Formatter.
     */
    private final OceanusDecimalParser theDecimalParser;

    /**
     * Extensions.
     */
    private final List<OceanusDataFormatterExtension> theExtensions;

    /**
     * Constructor.
     */
    public OceanusDataFormatter() {
        this(Locale.getDefault());
    }

    /**
     * Constructor.
     * @param pLocale the locale
     */
    public OceanusDataFormatter(final Locale pLocale) {
        theDateFormatter = new OceanusDateFormatter(pLocale);
        theDecimalFormatter = new OceanusDecimalFormatter(pLocale);
        theDecimalParser = new OceanusDecimalParser(pLocale);
        theExtensions = new ArrayList<>();
    }

    /**
     * Obtain the date formatter.
     * @return the formatter
     */
    public OceanusDateFormatter getDateFormatter() {
        return theDateFormatter;
    }

    /**
     * Obtain the decimal formatter.
     * @return the formatter
     */
    public OceanusDecimalFormatter getDecimalFormatter() {
        return theDecimalFormatter;
    }

    /**
     * Obtain the decimal parser.
     * @return the parser
     */
    public OceanusDecimalParser getDecimalParser() {
        return theDecimalParser;
    }

    /**
     * Extend the formatter.
     * @param pExtension the extension
     */
    public void extendFormatter(final OceanusDataFormatterExtension pExtension) {
        theExtensions.add(pExtension);
    }

    /**
     * Set accounting width.
     * @param pWidth the accounting width to use
     */
    public void setAccountingWidth(final int pWidth) {
        /* Set accounting width on decimal formatter */
        theDecimalFormatter.setAccountingWidth(pWidth);
    }

    /**
     * Clear accounting mode.
     */
    public void clearAccounting() {
        /* Clear the accounting mode flag */
        theDecimalFormatter.clearAccounting();
    }

    /**
     * Set the date format.
     * @param pFormat the format string
     */
    public final void setFormat(final String pFormat) {
        /* Tell the formatters about the format */
        theDateFormatter.setFormat(pFormat);
    }

    /**
     * Set the locale.
     * @param pLocale the locale
     */
    public final void setLocale(final Locale pLocale) {
        /* Tell the formatters about the locale */
        theDateFormatter.setLocale(pLocale);
        theDecimalFormatter.setLocale(pLocale);
        theDecimalParser.setLocale(pLocale);
    }

    /**
     * Obtain the locale.
     * @return the locale
     */
    public Locale getLocale() {
        /* Obtain locale from date formatter */
        return theDateFormatter.getLocale();
    }

    /**
     * Format an object value.
     * @param pValue the object to format
     * @return the formatted value
     */
    public String formatObject(final Object pValue) {
        /* Handle null value */
        if (pValue == null) {
            return null;
        }

        /* Loop through extensions */
        for (OceanusDataFormatterExtension myExtension : theExtensions) {
            final String myResult = myExtension.formatObject(pValue);
            if (myResult != null) {
                return myResult;
            }
        }

        /* Access the class */
        final Class<?> myClass = pValue.getClass();

        /* Handle Native classes */
        if (pValue instanceof String) {
            return (String) pValue;
        }
        if (pValue instanceof Boolean) {
            return Boolean.TRUE.equals(pValue)
                    ? "true"
                    : "false";
        }
        if (pValue instanceof Short
                || pValue instanceof Integer
                || pValue instanceof Long) {
            return pValue.toString();
        }
        if (pValue instanceof Float
                || pValue instanceof Double) {
            return pValue.toString();
        }
        if (pValue instanceof BigInteger
                || pValue instanceof BigDecimal) {
            return pValue.toString();
        }

        /* Handle Enumerated classes */
        if (pValue instanceof Enum) {
            return pValue.toString();
        }

        /* Handle Class */
        if (pValue instanceof Class) {
            return ((Class<?>) pValue).getCanonicalName();
        }

        /* Handle Native array classes */
        if (pValue instanceof byte[]) {
            return OceanusDataConverter.bytesToHexString((byte[]) pValue);
        }
        if (pValue instanceof char[]) {
            return new String((char[]) pValue);
        }

        /* Handle date classes */
        if (pValue instanceof Calendar) {
            return theDateFormatter.formatCalendarDay((Calendar) pValue);
        }
        if (pValue instanceof Date) {
            return theDateFormatter.formatJavaDate((Date) pValue);
        }
        if (pValue instanceof LocalDate) {
            return theDateFormatter.formatLocalDate((LocalDate) pValue);
        }
        if (pValue instanceof OceanusDate) {
            return theDateFormatter.formatDate((OceanusDate) pValue);
        }
        if (pValue instanceof OceanusDateRange) {
            return theDateFormatter.formatDateRange((OceanusDateRange) pValue);
        }

        /* Handle decimal classes */
        if (pValue instanceof OceanusDecimal) {
            return theDecimalFormatter.formatDecimal((OceanusDecimal) pValue);
        }


        /* Handle TethysProfile */
        if (pValue instanceof OceanusProfile) {
            /* Format the profile */
            final OceanusProfile myProfile = (OceanusProfile) pValue;
            return myProfile.getName()
                    + ": "
                    + (myProfile.isRunning()
                    ? myProfile.getStatus()
                    : myProfile.getElapsed());
        }

        /* Handle OceanusExceptions */
        if (pValue instanceof OceanusException) {
            return myClass.getSimpleName();
        }

        /* Standard format option */
        return formatBasicValue(pValue);
    }

    /**
     * Parse object value.
     * @param <T> the value type
     * @param pSource the source value
     * @param pClazz the value type class
     * @return the formatted value
     * @throws IllegalArgumentException on bad Date/Decimal format
     * @throws NumberFormatException on bad Integer format
     */
    public <T> T parseValue(final String pSource,
                            final Class<T> pClazz) {
        if (Boolean.class.equals(pClazz)) {
            return pClazz.cast(Boolean.parseBoolean(pSource));
        }
        if (Short.class.equals(pClazz)) {
            return pClazz.cast(Short.parseShort(pSource));
        }
        if (Integer.class.equals(pClazz)) {
            return pClazz.cast(Integer.parseInt(pSource));
        }
        if (Long.class.equals(pClazz)) {
            return pClazz.cast(Long.parseLong(pSource));
        }
        if (Float.class.equals(pClazz)) {
            return pClazz.cast(Float.parseFloat(pSource));
        }
        if (Double.class.equals(pClazz)) {
            return pClazz.cast(Double.parseDouble(pSource));
        }
        if (BigInteger.class.equals(pClazz)) {
            return pClazz.cast(new BigInteger(pSource));
        }
        if (BigDecimal.class.equals(pClazz)) {
            return pClazz.cast(new BigDecimal(pSource));
        }
        if (Date.class.equals(pClazz)) {
            /* Parse the date */
            return pClazz.cast(theDateFormatter.parseJavaDate(pSource));
        }
        if (OceanusDate.class.equals(pClazz)) {
            /* Parse the date */
            return pClazz.cast(theDateFormatter.parseDate(pSource));
        }
        if (Calendar.class.equals(pClazz)) {
            /* Parse the date */
            return pClazz.cast(theDateFormatter.parseCalendarDay(pSource));
        }
        if (LocalDate.class.equals(pClazz)) {
            /* Parse the date */
            return pClazz.cast(theDateFormatter.parseLocalDate(pSource));
        }
        if (OceanusPrice.class.equals(pClazz)) {
            /* Parse the price */
            return pClazz.cast(theDecimalParser.parsePriceValue(pSource));
        }
        if (OceanusMoney.class.equals(pClazz)) {
            /* Parse the money */
            return pClazz.cast(theDecimalParser.parseMoneyValue(pSource));
        }
        if (OceanusRate.class.equals(pClazz)) {
            /* Parse the rate */
            return pClazz.cast(theDecimalParser.parseRateValue(pSource));
        }
        if (OceanusUnits.class.equals(pClazz)) {
            /* Parse the units */
            return pClazz.cast(theDecimalParser.parseUnitsValue(pSource));
        }
        if (OceanusRatio.class.equals(pClazz)) {
            /* Parse the dilution */
            return pClazz.cast(theDecimalParser.parseRatioValue(pSource));
        }
        throw new IllegalArgumentException(ERROR_CLASS + pClazz.getSimpleName());
    }

    /**
     * Parse object value.
     * @param <T> the value type
     * @param pSource the source value
     * @param pClazz the value type class
     * @return the formatted value
     * @throws IllegalArgumentException on bad TethysDecimal format
     */
    public <T> T parseValue(final Double pSource,
                            final Class<T> pClazz) {
        if (OceanusPrice.class.equals(pClazz)) {
            /* Parse the price */
            return pClazz.cast(theDecimalParser.createPriceFromDouble(pSource));
        }
        if (OceanusMoney.class.equals(pClazz)) {
            /* Parse the money */
            return pClazz.cast(theDecimalParser.createMoneyFromDouble(pSource));
        }
        if (OceanusRate.class.equals(pClazz)) {
            /* Parse the rate */
            return pClazz.cast(theDecimalParser.createRateFromDouble(pSource));
        }
        if (OceanusUnits.class.equals(pClazz)) {
            /* Parse the units */
            return pClazz.cast(theDecimalParser.createUnitsFromDouble(pSource));
        }
        if (OceanusRatio.class.equals(pClazz)) {
            /* Parse the dilution */
            return pClazz.cast(theDecimalParser.createRatioFromDouble(pSource));
        }
        throw new IllegalArgumentException(ERROR_CLASS + pClazz.getSimpleName());
    }

    /**
     * Parse object value.
     * @param <T> the value type
     * @param pSource the source value
     * @param pCurrCode the currency code
     * @param pClazz the value type class
     * @return the formatted value
     * @throws IllegalArgumentException on bad TethysDecimal format
     */
    public <T> T parseValue(final Double pSource,
                            final String pCurrCode,
                            final Class<T> pClazz) {
        if (OceanusPrice.class.equals(pClazz)) {
            /* Parse the price */
            return pClazz.cast(theDecimalParser.createPriceFromDouble(pSource, pCurrCode));
        }
        if (OceanusMoney.class.equals(pClazz)) {
            /* Parse the money */
            return pClazz.cast(theDecimalParser.createMoneyFromDouble(pSource, pCurrCode));
        }
        throw new IllegalArgumentException(ERROR_CLASS + pClazz.getSimpleName());
    }

    /**
     * Format basic object.
     * @param pValue the object
     * @return the formatted value
     */
    private static String formatBasicValue(final Object pValue) {
        /* Access the class */
        final Class<?> myClass = pValue.getClass();

        /* Create basic result */
        final StringBuilder myBuilder = new StringBuilder();
        myBuilder.append(myClass.getCanonicalName());

        /* Handle list/map instances */
        if (pValue instanceof List) {
            formatSize(myBuilder, ((List<?>) pValue).size());
        } else if (pValue instanceof Map) {
            formatSize(myBuilder, ((Map<?, ?>) pValue).size());
        }

        /* Return the value */
        return myBuilder.toString();
    }

    /**
     * Format size.
     * @param pBuilder the string builder
     * @param pSize the size
     */
    private static void formatSize(final StringBuilder pBuilder,
                                   final Object pSize) {
        /* Append the size */
        pBuilder.append('(');
        pBuilder.append(pSize);
        pBuilder.append(')');
    }
}