Renamed 'conversion' package to 'conversions'.
authorMarco Zanon <info@marcozanon.com>
Sat, 2 Mar 2024 17:27:18 +0000 (17:27 +0000)
committerMarco Zanon <info@marcozanon.com>
Sat, 2 Mar 2024 17:27:18 +0000 (17:27 +0000)
13 files changed:
CHANGELOG
src/main/java/com/marcozanon/macaco/conversion/MConversionException.java [deleted file]
src/main/java/com/marcozanon/macaco/conversion/MDateConverter.java [deleted file]
src/main/java/com/marcozanon/macaco/conversion/MInvalidConversionFormatException.java [deleted file]
src/main/java/com/marcozanon/macaco/conversion/MLocalDateConverter.java [deleted file]
src/main/java/com/marcozanon/macaco/conversion/MLocalDateTimeConverter.java [deleted file]
src/main/java/com/marcozanon/macaco/conversion/MNumberConverter.java [deleted file]
src/main/java/com/marcozanon/macaco/conversions/MConversionException.java [new file with mode: 0644]
src/main/java/com/marcozanon/macaco/conversions/MDateConverter.java [new file with mode: 0644]
src/main/java/com/marcozanon/macaco/conversions/MInvalidConversionFormatException.java [new file with mode: 0644]
src/main/java/com/marcozanon/macaco/conversions/MLocalDateConverter.java [new file with mode: 0644]
src/main/java/com/marcozanon/macaco/conversions/MLocalDateTimeConverter.java [new file with mode: 0644]
src/main/java/com/marcozanon/macaco/conversions/MNumberConverter.java [new file with mode: 0644]

index cab32f43f1110a6431c6d7b239d07bbe2a7543c3..1169b689fe453728b9aed1ada0a48b69c8b4da05 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,6 +5,7 @@ See LICENSE for details.
 ===================
 10.0.0 (2024-03-02)
 ===================
+* Renamed 'conversion' package to 'conversions'.
 * Renamed some methods and variables.
 
 ------------------
diff --git a/src/main/java/com/marcozanon/macaco/conversion/MConversionException.java b/src/main/java/com/marcozanon/macaco/conversion/MConversionException.java
deleted file mode 100644 (file)
index f4c825f..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Macaco
- * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
- * See LICENSE for details.
- */
-
-package com.marcozanon.macaco.conversion;
-
-import com.marcozanon.macaco.MException;
-
-public abstract class MConversionException extends MException {
-
-    /* */
-
-    public MConversionException() {
-        super();
-    }
-
-    public MConversionException(String message) {
-        super(message);
-    }
-
-    public MConversionException(Throwable error) {
-        super(error);
-    }
-
-    public MConversionException(String message, Throwable error) {
-        super(message, error);
-    }
-
-}
diff --git a/src/main/java/com/marcozanon/macaco/conversion/MDateConverter.java b/src/main/java/com/marcozanon/macaco/conversion/MDateConverter.java
deleted file mode 100644 (file)
index d0e6b05..0000000
+++ /dev/null
@@ -1,255 +0,0 @@
-/**
- * Macaco
- * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
- * See LICENSE for details.
- */
-
-package com.marcozanon.macaco.conversion;
-
-import com.marcozanon.macaco.MObject;
-import com.marcozanon.macaco.text.MText;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.LinkedHashSet;
-import java.util.Locale;
-import java.util.TimeZone;
-
-public class MDateConverter extends MObject {
-
-    protected LinkedHashSet<String> dateFormats = new LinkedHashSet<String>();
-    protected Locale locale = null;
-    protected TimeZone timeZone = null;
-
-    /* */
-
-    public MDateConverter(String defaultDateFormat, Locale locale) {
-        this(defaultDateFormat, locale, TimeZone.getDefault());
-    }
-
-    public MDateConverter(String defaultDateFormat, Locale locale, TimeZone timeZone) {
-        super();
-        //
-        this.addDateFormat(defaultDateFormat);
-        this.setLocale(locale);
-        this.setTimeZone(timeZone);
-    }
-
-    public MDateConverter(LinkedHashSet<String> dateFormats, Locale locale, TimeZone timeZone) {
-        super();
-        //
-        this.setDateFormats(dateFormats);
-        this.setLocale(locale);
-        this.setTimeZone(timeZone);
-    }
-
-    @Override
-    public MDateConverter clone() {
-        return new MDateConverter(this.getDateFormats(), this.getLocale(), this.getTimeZone());
-    }
-
-    /* Date formats. */
-
-    public void setDateFormats(LinkedHashSet<String> dateFormats) {
-        if (null == dateFormats) {
-            throw new IllegalArgumentException("Invalid 'dateFormats': null.");
-        }
-        else if (0 == dateFormats.size()) {
-            throw new IllegalArgumentException("Invalid 'dateFormats': empty.");
-        }
-        else {
-            for (String dateFormat: dateFormats) {
-                MDateConverter.checkDateFormat(dateFormat);
-            }
-        }
-        //
-        synchronized (this.dateFormats) {
-            this.dateFormats = dateFormats;
-        }
-    }
-
-    public void addDateFormat(String dateFormat) {
-        MDateConverter.checkDateFormat(dateFormat);
-        //
-        synchronized (this.dateFormats) {
-            this.dateFormats.add(dateFormat);
-        }
-    }
-
-    public LinkedHashSet<String> getDateFormats() {
-        return this.dateFormats;
-    }
-
-    public String getDefaultDateFormat() {
-        return this.getDateFormats().iterator().next();
-    }
-
-    public LinkedHashSet<String> getSecondaryDateFormats() {
-        LinkedHashSet<String> dateFormats = (LinkedHashSet<String>)this.getDateFormats().clone();
-        //
-        dateFormats.remove(this.getDefaultDateFormat());
-        //
-        return dateFormats;
-    }
-
-    /* Locale. */
-
-    protected void setLocale(Locale locale) {
-        if (null == locale) {
-            throw new IllegalArgumentException("Invalid 'locale': null.");
-        }
-        //
-        this.locale = locale;
-    }
-
-    public Locale getLocale() {
-        return this.locale;
-    }
-
-    /* Time zone. */
-
-    protected void setTimeZone(TimeZone timeZone) {
-        if (null == timeZone) {
-            throw new IllegalArgumentException("Invalid 'timeZone': null.");
-        }
-        //
-        this.timeZone = timeZone;
-    }
-
-    public TimeZone getTimeZone() {
-        return this.timeZone;
-    }
-
-    /* Conversions. */
-
-    protected static void checkDateFormat(String dateFormat) {
-        if (MText.isBlank(dateFormat)) {
-            throw new IllegalArgumentException("Invalid 'dateFormat': null or empty.");
-        }
-        //
-        try {
-            SimpleDateFormat testDateFormat = new SimpleDateFormat(dateFormat);
-        }
-        catch (IllegalArgumentException exception) {
-            throw new IllegalArgumentException(String.format("Invalid 'dateFormat': %s.", dateFormat)); // no need to propagate exception
-        }
-    }
-
-    protected static Date createDateFromString(String x, String inputDateFormat, Locale inputLocale, TimeZone inputTimeZone) throws MInvalidConversionFormatException {
-        if (MText.isBlank(x)) {
-            throw new IllegalArgumentException("Invalid 'x': null or empty.");
-        }
-        MDateConverter.checkDateFormat(inputDateFormat);
-        if (null == inputLocale) {
-            throw new IllegalArgumentException("Invalid 'inputLocale': null.");
-        }
-        if (null == inputTimeZone) {
-            throw new IllegalArgumentException("Invalid 'inputTimeZone': null.");
-        }
-        //
-        Calendar c1 = Calendar.getInstance(inputTimeZone, inputLocale);
-        c1.clear();
-        c1.setLenient(false);
-        SimpleDateFormat sdf = new SimpleDateFormat(inputDateFormat, inputLocale);
-        sdf.setCalendar(c1);
-        //
-        Date d1 = null;
-        try {
-            d1 = sdf.parse(x);
-        }
-        catch (ParseException exception) {
-            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDateFormat)); // no need to propagate exception
-        }
-        //
-        Calendar c2 = Calendar.getInstance(inputTimeZone, inputLocale);
-        c2.clear();
-        c2.set(Calendar.YEAR, c1.get(Calendar.YEAR));
-        c2.set(Calendar.MONTH, c1.get(Calendar.MONTH));
-        c2.set(Calendar.DAY_OF_MONTH, c1.get(Calendar.DAY_OF_MONTH));
-        c2.set(Calendar.HOUR_OF_DAY, c1.get(Calendar.HOUR_OF_DAY));
-        c2.set(Calendar.MINUTE, c1.get(Calendar.MINUTE));
-        c2.set(Calendar.SECOND, c1.get(Calendar.SECOND));
-        //
-        Date d2 = c2.getTime();
-        if (!x.equals(sdf.format(d2))) {
-            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDateFormat));
-        }
-        //
-        return d2;
-    }
-
-    protected static String createStringFromDate(Date date, String outputDateFormat, Locale outputLocale, TimeZone outputTimeZone) {
-        if (null == date) {
-            throw new IllegalArgumentException("Invalid 'date': null.");
-        }
-        MDateConverter.checkDateFormat(outputDateFormat);
-        if (null == outputLocale) {
-            throw new IllegalArgumentException("Invalid 'outputLocale': null.");
-        }
-        if (null == outputTimeZone) {
-            throw new IllegalArgumentException("Invalid 'outputTimeZone': null.");
-        }
-        //
-        SimpleDateFormat sdf = new SimpleDateFormat(outputDateFormat, outputLocale);
-        sdf.setCalendar(Calendar.getInstance(outputTimeZone, outputLocale));
-        //
-        return sdf.format(date);
-    }
-
-    public Date createDateFromString(String x) throws MInvalidConversionFormatException {
-        Date y = null;
-        for (String dateFormat: this.getDateFormats()) {
-            try {
-                y = MDateConverter.createDateFromString(x, dateFormat, this.getLocale(), this.getTimeZone());
-                //
-                return y;
-            }
-            catch (MInvalidConversionFormatException exception) {
-            }
-        }
-        if (null == y) {
-            throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x));
-        }
-        //
-        return y; // necessary to avoid Java compilation errors
-    }
-
-    public String createStringFromDate(Date x) {
-        return MDateConverter.createStringFromDate(x, this.getDefaultDateFormat(), this.getLocale(), this.getTimeZone());
-    }
-
-    /* Helpers. */
-
-    public static Date getFlatDate(Date x) {
-        if (null == x) {
-            return x;
-        }
-        //
-        Calendar calendar = new GregorianCalendar();
-        calendar.setTime(x);
-        calendar.set(Calendar.HOUR_OF_DAY, 0);
-        calendar.set(Calendar.MINUTE, 0);
-        calendar.set(Calendar.SECOND, 0);
-        calendar.set(Calendar.MILLISECOND, 0);
-        //
-        return calendar.getTime();
-    }
-
-    public static Date getCeilDate(Date x) {
-        if (null == x) {
-            return x;
-        }
-        //
-        Calendar calendar = new GregorianCalendar();
-        calendar.setTime(x);
-        calendar.set(Calendar.HOUR_OF_DAY, 23);
-        calendar.set(Calendar.MINUTE, 59);
-        calendar.set(Calendar.SECOND, 59);
-        calendar.set(Calendar.MILLISECOND, 999);
-        //
-        return calendar.getTime();
-    }
-
-}
diff --git a/src/main/java/com/marcozanon/macaco/conversion/MInvalidConversionFormatException.java b/src/main/java/com/marcozanon/macaco/conversion/MInvalidConversionFormatException.java
deleted file mode 100644 (file)
index e23c0b1..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Macaco
- * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
- * See LICENSE for details.
- */
-
-package com.marcozanon.macaco.conversion;
-
-@SuppressWarnings("serial")
-public class MInvalidConversionFormatException extends MConversionException {
-
-    /* */
-
-    public MInvalidConversionFormatException() {
-        super();
-    }
-
-    public MInvalidConversionFormatException(String message) {
-        super(message);
-    }
-
-    public MInvalidConversionFormatException(Throwable error) {
-        super(error);
-    }
-
-    public MInvalidConversionFormatException(String message, Throwable error) {
-        super(message, error);
-    }
-
-}
diff --git a/src/main/java/com/marcozanon/macaco/conversion/MLocalDateConverter.java b/src/main/java/com/marcozanon/macaco/conversion/MLocalDateConverter.java
deleted file mode 100644 (file)
index 6f06a46..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-/**
- * Macaco
- * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
- * See LICENSE for details.
- */
-
-package com.marcozanon.macaco.conversion;
-
-import com.marcozanon.macaco.MObject;
-import com.marcozanon.macaco.text.MText;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
-import java.time.format.ResolverStyle;
-import java.util.LinkedHashSet;
-import java.util.Locale;
-
-public class MLocalDateConverter extends MObject {
-
-    protected LinkedHashSet<String> dateFormats = new LinkedHashSet<String>();
-    protected Locale locale = null;
-
-    /* */
-
-    public MLocalDateConverter(String defaultDateFormat, Locale locale) {
-        super();
-        //
-        this.addDateFormat(defaultDateFormat);
-        this.setLocale(locale);
-    }
-
-    public MLocalDateConverter(LinkedHashSet<String> dateFormats, Locale locale) {
-        super();
-        //
-        this.setDateFormats(dateFormats);
-        this.setLocale(locale);
-    }
-
-    @Override
-    public MLocalDateConverter clone() {
-        return new MLocalDateConverter(this.getDateFormats(), this.getLocale());
-    }
-
-    /* Date formats. */
-
-    public void setDateFormats(LinkedHashSet<String> dateFormats) {
-        if (null == dateFormats) {
-            throw new IllegalArgumentException("Invalid 'dateFormats': null.");
-        }
-        else if (0 == dateFormats.size()) {
-            throw new IllegalArgumentException("Invalid 'dateFormats': empty.");
-        }
-        else {
-            for (String dateFormat: dateFormats) {
-                MLocalDateConverter.checkDateFormat(dateFormat);
-            }
-        }
-        //
-        synchronized (this.dateFormats) {
-            this.dateFormats = dateFormats;
-        }
-    }
-
-    public void addDateFormat(String dateFormat) {
-        MLocalDateConverter.checkDateFormat(dateFormat);
-        //
-        synchronized (this.dateFormats) {
-            this.dateFormats.add(dateFormat);
-        }
-    }
-
-    public LinkedHashSet<String> getDateFormats() {
-        return this.dateFormats;
-    }
-
-    public String getDefaultDateFormat() {
-        return this.getDateFormats().iterator().next();
-    }
-
-    public LinkedHashSet<String> getSecondaryDateFormats() {
-        LinkedHashSet<String> dateFormats = (LinkedHashSet<String>)this.getDateFormats().clone();
-        //
-        dateFormats.remove(this.getDefaultDateFormat());
-        //
-        return dateFormats;
-    }
-
-    /* Locale. */
-
-    protected void setLocale(Locale locale) {
-        if (null == locale) {
-            throw new IllegalArgumentException("Invalid 'locale': null.");
-        }
-        //
-        this.locale = locale;
-    }
-
-    public Locale getLocale() {
-        return this.locale;
-    }
-
-    /* Conversions. */
-
-    protected static void checkDateFormat(String dateFormat) {
-        if (MText.isBlank(dateFormat)) {
-            throw new IllegalArgumentException("Invalid 'dateFormat': null or empty.");
-        }
-        //
-        try {
-            DateTimeFormatter.ofPattern(dateFormat).withResolverStyle(ResolverStyle.STRICT);
-        }
-        catch (IllegalArgumentException exception) {
-            throw new IllegalArgumentException(String.format("Invalid 'dateFormat': %s.", dateFormat)); // no need to propagate exception
-        }
-    }
-
-    protected static LocalDate createDateFromString(String x, String inputDateFormat, Locale inputLocale) throws MInvalidConversionFormatException {
-        if (MText.isBlank(x)) {
-            throw new IllegalArgumentException("Invalid 'x': null or empty.");
-        }
-        MLocalDateConverter.checkDateFormat(inputDateFormat);
-        if (null == inputLocale) {
-            throw new IllegalArgumentException("Invalid 'inputLocale': null.");
-        }
-        //
-        LocalDate d = null;
-        try {
-            d = LocalDate.parse(x, DateTimeFormatter.ofPattern(inputDateFormat, inputLocale).withResolverStyle(ResolverStyle.STRICT));
-        }
-        catch (DateTimeParseException exception) {
-            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDateFormat)); // no need to propagate exception
-        }
-        //
-        return d;
-    }
-
-    protected static String createStringFromDate(LocalDate date, String outputDateFormat, Locale outputLocale) {
-        if (null == date) {
-            throw new IllegalArgumentException("Invalid 'date': null.");
-        }
-        MLocalDateConverter.checkDateFormat(outputDateFormat);
-        if (null == outputLocale) {
-            throw new IllegalArgumentException("Invalid 'outputLocale': null.");
-        }
-        //
-        return date.format(DateTimeFormatter.ofPattern(outputDateFormat, outputLocale).withResolverStyle(ResolverStyle.STRICT));
-    }
-
-    public LocalDate createDateFromString(String x) throws MInvalidConversionFormatException {
-        LocalDate y = null;
-        for (String dateFormat: this.getDateFormats()) {
-            try {
-                y = MLocalDateConverter.createDateFromString(x, dateFormat, this.getLocale());
-                //
-                return y;
-            }
-            catch (MInvalidConversionFormatException exception) {
-            }
-        }
-        if (null == y) {
-            throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x));
-        }
-        //
-        return y; // necessary to avoid Java compilation errors
-    }
-
-    public String createStringFromDate(LocalDate x) {
-        return MLocalDateConverter.createStringFromDate(x, this.getDefaultDateFormat(), this.getLocale());
-    }
-
-}
diff --git a/src/main/java/com/marcozanon/macaco/conversion/MLocalDateTimeConverter.java b/src/main/java/com/marcozanon/macaco/conversion/MLocalDateTimeConverter.java
deleted file mode 100644 (file)
index 00fae55..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-/**
- * Macaco
- * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
- * See LICENSE for details.
- */
-
-package com.marcozanon.macaco.conversion;
-
-import com.marcozanon.macaco.MObject;
-import com.marcozanon.macaco.text.MText;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
-import java.time.format.ResolverStyle;
-import java.util.LinkedHashSet;
-import java.util.Locale;
-
-public class MLocalDateTimeConverter extends MObject {
-
-    protected LinkedHashSet<String> datetimeFormats = new LinkedHashSet<String>();
-    protected Locale locale = null;
-
-    /* */
-
-    public MLocalDateTimeConverter(String defaultDatetimeFormat, Locale locale) {
-        super();
-        //
-        this.addDatetimeFormat(defaultDatetimeFormat);
-        this.setLocale(locale);
-    }
-
-    public MLocalDateTimeConverter(LinkedHashSet<String> datetimeFormats, Locale locale) {
-        super();
-        //
-        this.setDatetimeFormats(datetimeFormats);
-        this.setLocale(locale);
-    }
-
-    @Override
-    public MLocalDateTimeConverter clone() {
-        return new MLocalDateTimeConverter(this.getDatetimeFormats(), this.getLocale());
-    }
-
-    /* Datetime formats. */
-
-    public void setDatetimeFormats(LinkedHashSet<String> datetimeFormats) {
-        if (null == datetimeFormats) {
-            throw new IllegalArgumentException("Invalid 'datetimeFormats': null.");
-        }
-        else if (0 == datetimeFormats.size()) {
-            throw new IllegalArgumentException("Invalid 'datetimeFormats': empty.");
-        }
-        else {
-            for (String datetimeFormat: datetimeFormats) {
-                MLocalDateTimeConverter.checkDatetimeFormat(datetimeFormat);
-            }
-        }
-        //
-        synchronized (this.datetimeFormats) {
-            this.datetimeFormats = datetimeFormats;
-        }
-    }
-
-    public void addDatetimeFormat(String datetimeFormat) {
-        MLocalDateTimeConverter.checkDatetimeFormat(datetimeFormat);
-        //
-        synchronized (this.datetimeFormats) {
-            this.datetimeFormats.add(datetimeFormat);
-        }
-    }
-
-    public LinkedHashSet<String> getDatetimeFormats() {
-        return this.datetimeFormats;
-    }
-
-    public String getDefaultDatetimeFormat() {
-        return this.getDatetimeFormats().iterator().next();
-    }
-
-    public LinkedHashSet<String> getSecondaryDatetimeFormats() {
-        LinkedHashSet<String> datetimeFormats = (LinkedHashSet<String>)this.getDatetimeFormats().clone();
-        //
-        datetimeFormats.remove(this.getDefaultDatetimeFormat());
-        //
-        return datetimeFormats;
-    }
-
-    /* Locale. */
-
-    protected void setLocale(Locale locale) {
-        if (null == locale) {
-            throw new IllegalArgumentException("Invalid 'locale': null.");
-        }
-        //
-        this.locale = locale;
-    }
-
-    public Locale getLocale() {
-        return this.locale;
-    }
-
-    /* Conversions. */
-
-    protected static void checkDatetimeFormat(String datetimeFormat) {
-        if (MText.isBlank(datetimeFormat)) {
-            throw new IllegalArgumentException("Invalid 'datetimeFormat': null or empty.");
-        }
-        //
-        try {
-            DateTimeFormatter.ofPattern(datetimeFormat).withResolverStyle(ResolverStyle.STRICT);
-        }
-        catch (IllegalArgumentException exception) {
-            throw new IllegalArgumentException(String.format("Invalid 'datetimeFormat': %s.", datetimeFormat)); // no need to propagate exception
-        }
-    }
-
-    protected static LocalDateTime createDatetimeFromString(String x, String inputDatetimeFormat, Locale inputLocale) throws MInvalidConversionFormatException {
-        if (MText.isBlank(x)) {
-            throw new IllegalArgumentException("Invalid 'x': null or empty.");
-        }
-        MLocalDateTimeConverter.checkDatetimeFormat(inputDatetimeFormat);
-        if (null == inputLocale) {
-            throw new IllegalArgumentException("Invalid 'inputLocale': null.");
-        }
-        //
-        LocalDateTime dt = null;
-        try {
-            dt = LocalDateTime.parse(x, DateTimeFormatter.ofPattern(inputDatetimeFormat, inputLocale).withResolverStyle(ResolverStyle.STRICT));
-        }
-        catch (DateTimeParseException exception) {
-            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDatetimeFormat)); // no need to propagate exception
-        }
-        //
-        return dt;
-    }
-
-    protected static String createStringFromDatetime(LocalDateTime datetime, String outputDatetimeFormat, Locale outputLocale) {
-        if (null == datetime) {
-            throw new IllegalArgumentException("Invalid 'datetime': null.");
-        }
-        MLocalDateTimeConverter.checkDatetimeFormat(outputDatetimeFormat);
-        if (null == outputLocale) {
-            throw new IllegalArgumentException("Invalid 'outputLocale': null.");
-        }
-        //
-        return datetime.format(DateTimeFormatter.ofPattern(outputDatetimeFormat, outputLocale).withResolverStyle(ResolverStyle.STRICT));
-    }
-
-    public LocalDateTime createDatetimeFromString(String x) throws MInvalidConversionFormatException {
-        LocalDateTime y = null;
-        for (String datetimeFormat: this.getDatetimeFormats()) {
-            try {
-                y = MLocalDateTimeConverter.createDatetimeFromString(x, datetimeFormat, this.getLocale());
-                //
-                return y;
-            }
-            catch (MInvalidConversionFormatException exception) {
-            }
-        }
-        if (null == y) {
-            throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x));
-        }
-        //
-        return y; // necessary to avoid Java compilation errors
-    }
-
-    public String createStringFromDatetime(LocalDateTime x) {
-        return MLocalDateTimeConverter.createStringFromDatetime(x, this.getDefaultDatetimeFormat(), this.getLocale());
-    }
-
-}
diff --git a/src/main/java/com/marcozanon/macaco/conversion/MNumberConverter.java b/src/main/java/com/marcozanon/macaco/conversion/MNumberConverter.java
deleted file mode 100644 (file)
index d599c08..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-/**
- * Macaco
- * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
- * See LICENSE for details.
- */
-
-package com.marcozanon.macaco.conversion;
-
-import com.marcozanon.macaco.MObject;
-import com.marcozanon.macaco.text.MText;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-import java.text.ParsePosition;
-import java.util.LinkedHashSet;
-import java.util.Locale;
-
-public class MNumberConverter extends MObject {
-
-    protected LinkedHashSet<String> numberFormats = new LinkedHashSet<String>();
-    protected Locale locale = null;
-
-    /* */
-
-    public MNumberConverter(String defaultNumberFormat, Locale locale) {
-        super();
-        //
-        this.addNumberFormat(defaultNumberFormat);
-        this.setLocale(locale);
-    }
-
-    public MNumberConverter(LinkedHashSet<String> numberFormats, Locale locale) {
-        super();
-        //
-        this.setNumberFormats(numberFormats);
-        this.setLocale(locale);
-    }
-
-    @Override
-    public MNumberConverter clone() {
-        return new MNumberConverter(this.getNumberFormats(), this.getLocale());
-    }
-
-    /* Number formats. */
-
-    public void setNumberFormats(LinkedHashSet<String> numberFormats) {
-        if (null == numberFormats) {
-            throw new IllegalArgumentException("Invalid 'numberFormats': null.");
-        }
-        else if (0 == numberFormats.size()) {
-            throw new IllegalArgumentException("Invalid 'numberFormats': empty.");
-        }
-        else {
-            for (String numberFormat: numberFormats) {
-                MNumberConverter.checkNumberFormat(numberFormat);
-            }
-        }
-        //
-        synchronized (this.numberFormats) {
-            this.numberFormats = numberFormats;
-        }
-    }
-
-    public void addNumberFormat(String numberFormat) {
-        MNumberConverter.checkNumberFormat(numberFormat);
-        //
-        synchronized (this.numberFormats) {
-            this.numberFormats.add(numberFormat);
-        }
-    }
-
-    public LinkedHashSet<String> getNumberFormats() {
-        return this.numberFormats;
-    }
-
-    public String getDefaultNumberFormat() {
-        return this.getNumberFormats().iterator().next();
-    }
-
-    public LinkedHashSet<String> getSecondaryNumberFormats() {
-        LinkedHashSet<String> numberFormats = (LinkedHashSet<String>)this.getNumberFormats().clone();
-        //
-        numberFormats.remove(this.getDefaultNumberFormat());
-        //
-        return numberFormats;
-    }
-
-    /* Locale. */
-
-    protected void setLocale(Locale locale) {
-        if (null == locale) {
-            throw new IllegalArgumentException("Invalid 'locale': null.");
-        }
-        //
-        this.locale = locale;
-    }
-
-    public Locale getLocale() {
-        return this.locale;
-    }
-
-    /* Conversions. */
-
-    protected static void checkNumberFormat(String numberFormat) {
-        if (MText.isBlank(numberFormat)) {
-            throw new IllegalArgumentException("Invalid 'numberFormat': null or empty.");
-        }
-        //
-        try {
-            DecimalFormat testNumberFormat = new DecimalFormat(numberFormat);
-        }
-        catch (IllegalArgumentException exception) {
-            throw new IllegalArgumentException(String.format("Invalid 'numberFormat': %s.", numberFormat)); // no need to propagate exception
-        }
-    }
-
-    protected static BigDecimal createNumberFromString(String x, String inputNumberFormat, Locale inputLocale) throws MInvalidConversionFormatException {
-        if (MText.isBlank(x)) {
-            throw new IllegalArgumentException("Invalid 'x': null or empty.");
-        }
-        MNumberConverter.checkNumberFormat(inputNumberFormat);
-        if (null == inputLocale) {
-            throw new IllegalArgumentException("Invalid 'inputLocale': null.");
-        }
-        //
-        DecimalFormatSymbols dfs = new DecimalFormatSymbols(inputLocale);
-        DecimalFormat df = new DecimalFormat(inputNumberFormat, dfs);
-        df.setParseBigDecimal(true);
-        //
-        ParsePosition validPosition = new ParsePosition(0);
-        BigDecimal bd = (BigDecimal)df.parse(x, validPosition);
-        if (validPosition.getIndex() < x.length()) {
-            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputNumberFormat));
-        }
-        //
-        return bd;
-    }
-
-    protected static String createStringFromNumber(BigDecimal number, String outputNumberFormat, Locale outputLocale) {
-        if (null == number) {
-            throw new IllegalArgumentException("Invalid 'number': null.");
-        }
-        MNumberConverter.checkNumberFormat(outputNumberFormat);
-        if (null == outputLocale) {
-            throw new IllegalArgumentException("Invalid 'outputLocale': null.");
-        }
-        //
-        DecimalFormatSymbols dfs = new DecimalFormatSymbols(outputLocale);
-        DecimalFormat df = new DecimalFormat(outputNumberFormat, dfs);
-        df.setRoundingMode(RoundingMode.HALF_UP);
-        //
-        return df.format(number);
-    }
-
-    public BigDecimal createNumberFromString(String x) throws MInvalidConversionFormatException {
-        BigDecimal y = null;
-        for (String numberFormat: this.getNumberFormats()) {
-            try {
-                y = MNumberConverter.createNumberFromString(x, numberFormat, this.getLocale());
-                //
-                return y;
-            }
-            catch (MInvalidConversionFormatException exception) {
-            }
-        }
-        if (null == y) {
-            throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x));
-        }
-        //
-        return y; // necessary to avoid Java compilation errors
-    }
-
-    public String createStringFromNumber(BigDecimal x) {
-        return MNumberConverter.createStringFromNumber(x, this.getDefaultNumberFormat(), this.getLocale());
-    }
-
-}
diff --git a/src/main/java/com/marcozanon/macaco/conversions/MConversionException.java b/src/main/java/com/marcozanon/macaco/conversions/MConversionException.java
new file mode 100644 (file)
index 0000000..01a8f72
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Macaco
+ * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
+ * See LICENSE for details.
+ */
+
+package com.marcozanon.macaco.conversions;
+
+import com.marcozanon.macaco.MException;
+
+public abstract class MConversionException extends MException {
+
+    /* */
+
+    public MConversionException() {
+        super();
+    }
+
+    public MConversionException(String message) {
+        super(message);
+    }
+
+    public MConversionException(Throwable error) {
+        super(error);
+    }
+
+    public MConversionException(String message, Throwable error) {
+        super(message, error);
+    }
+
+}
diff --git a/src/main/java/com/marcozanon/macaco/conversions/MDateConverter.java b/src/main/java/com/marcozanon/macaco/conversions/MDateConverter.java
new file mode 100644 (file)
index 0000000..959a4bc
--- /dev/null
@@ -0,0 +1,255 @@
+/**
+ * Macaco
+ * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
+ * See LICENSE for details.
+ */
+
+package com.marcozanon.macaco.conversions;
+
+import com.marcozanon.macaco.MObject;
+import com.marcozanon.macaco.text.MText;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class MDateConverter extends MObject {
+
+    protected LinkedHashSet<String> dateFormats = new LinkedHashSet<String>();
+    protected Locale locale = null;
+    protected TimeZone timeZone = null;
+
+    /* */
+
+    public MDateConverter(String defaultDateFormat, Locale locale) {
+        this(defaultDateFormat, locale, TimeZone.getDefault());
+    }
+
+    public MDateConverter(String defaultDateFormat, Locale locale, TimeZone timeZone) {
+        super();
+        //
+        this.addDateFormat(defaultDateFormat);
+        this.setLocale(locale);
+        this.setTimeZone(timeZone);
+    }
+
+    public MDateConverter(LinkedHashSet<String> dateFormats, Locale locale, TimeZone timeZone) {
+        super();
+        //
+        this.setDateFormats(dateFormats);
+        this.setLocale(locale);
+        this.setTimeZone(timeZone);
+    }
+
+    @Override
+    public MDateConverter clone() {
+        return new MDateConverter(this.getDateFormats(), this.getLocale(), this.getTimeZone());
+    }
+
+    /* Date formats. */
+
+    public void setDateFormats(LinkedHashSet<String> dateFormats) {
+        if (null == dateFormats) {
+            throw new IllegalArgumentException("Invalid 'dateFormats': null.");
+        }
+        else if (0 == dateFormats.size()) {
+            throw new IllegalArgumentException("Invalid 'dateFormats': empty.");
+        }
+        else {
+            for (String dateFormat: dateFormats) {
+                MDateConverter.checkDateFormat(dateFormat);
+            }
+        }
+        //
+        synchronized (this.dateFormats) {
+            this.dateFormats = dateFormats;
+        }
+    }
+
+    public void addDateFormat(String dateFormat) {
+        MDateConverter.checkDateFormat(dateFormat);
+        //
+        synchronized (this.dateFormats) {
+            this.dateFormats.add(dateFormat);
+        }
+    }
+
+    public LinkedHashSet<String> getDateFormats() {
+        return this.dateFormats;
+    }
+
+    public String getDefaultDateFormat() {
+        return this.getDateFormats().iterator().next();
+    }
+
+    public LinkedHashSet<String> getSecondaryDateFormats() {
+        LinkedHashSet<String> dateFormats = (LinkedHashSet<String>)this.getDateFormats().clone();
+        //
+        dateFormats.remove(this.getDefaultDateFormat());
+        //
+        return dateFormats;
+    }
+
+    /* Locale. */
+
+    protected void setLocale(Locale locale) {
+        if (null == locale) {
+            throw new IllegalArgumentException("Invalid 'locale': null.");
+        }
+        //
+        this.locale = locale;
+    }
+
+    public Locale getLocale() {
+        return this.locale;
+    }
+
+    /* Time zone. */
+
+    protected void setTimeZone(TimeZone timeZone) {
+        if (null == timeZone) {
+            throw new IllegalArgumentException("Invalid 'timeZone': null.");
+        }
+        //
+        this.timeZone = timeZone;
+    }
+
+    public TimeZone getTimeZone() {
+        return this.timeZone;
+    }
+
+    /* Conversions. */
+
+    protected static void checkDateFormat(String dateFormat) {
+        if (MText.isBlank(dateFormat)) {
+            throw new IllegalArgumentException("Invalid 'dateFormat': null or empty.");
+        }
+        //
+        try {
+            SimpleDateFormat testDateFormat = new SimpleDateFormat(dateFormat);
+        }
+        catch (IllegalArgumentException exception) {
+            throw new IllegalArgumentException(String.format("Invalid 'dateFormat': %s.", dateFormat)); // no need to propagate exception
+        }
+    }
+
+    protected static Date createDateFromString(String x, String inputDateFormat, Locale inputLocale, TimeZone inputTimeZone) throws MInvalidConversionFormatException {
+        if (MText.isBlank(x)) {
+            throw new IllegalArgumentException("Invalid 'x': null or empty.");
+        }
+        MDateConverter.checkDateFormat(inputDateFormat);
+        if (null == inputLocale) {
+            throw new IllegalArgumentException("Invalid 'inputLocale': null.");
+        }
+        if (null == inputTimeZone) {
+            throw new IllegalArgumentException("Invalid 'inputTimeZone': null.");
+        }
+        //
+        Calendar c1 = Calendar.getInstance(inputTimeZone, inputLocale);
+        c1.clear();
+        c1.setLenient(false);
+        SimpleDateFormat sdf = new SimpleDateFormat(inputDateFormat, inputLocale);
+        sdf.setCalendar(c1);
+        //
+        Date d1 = null;
+        try {
+            d1 = sdf.parse(x);
+        }
+        catch (ParseException exception) {
+            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDateFormat)); // no need to propagate exception
+        }
+        //
+        Calendar c2 = Calendar.getInstance(inputTimeZone, inputLocale);
+        c2.clear();
+        c2.set(Calendar.YEAR, c1.get(Calendar.YEAR));
+        c2.set(Calendar.MONTH, c1.get(Calendar.MONTH));
+        c2.set(Calendar.DAY_OF_MONTH, c1.get(Calendar.DAY_OF_MONTH));
+        c2.set(Calendar.HOUR_OF_DAY, c1.get(Calendar.HOUR_OF_DAY));
+        c2.set(Calendar.MINUTE, c1.get(Calendar.MINUTE));
+        c2.set(Calendar.SECOND, c1.get(Calendar.SECOND));
+        //
+        Date d2 = c2.getTime();
+        if (!x.equals(sdf.format(d2))) {
+            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDateFormat));
+        }
+        //
+        return d2;
+    }
+
+    protected static String createStringFromDate(Date date, String outputDateFormat, Locale outputLocale, TimeZone outputTimeZone) {
+        if (null == date) {
+            throw new IllegalArgumentException("Invalid 'date': null.");
+        }
+        MDateConverter.checkDateFormat(outputDateFormat);
+        if (null == outputLocale) {
+            throw new IllegalArgumentException("Invalid 'outputLocale': null.");
+        }
+        if (null == outputTimeZone) {
+            throw new IllegalArgumentException("Invalid 'outputTimeZone': null.");
+        }
+        //
+        SimpleDateFormat sdf = new SimpleDateFormat(outputDateFormat, outputLocale);
+        sdf.setCalendar(Calendar.getInstance(outputTimeZone, outputLocale));
+        //
+        return sdf.format(date);
+    }
+
+    public Date createDateFromString(String x) throws MInvalidConversionFormatException {
+        Date y = null;
+        for (String dateFormat: this.getDateFormats()) {
+            try {
+                y = MDateConverter.createDateFromString(x, dateFormat, this.getLocale(), this.getTimeZone());
+                //
+                return y;
+            }
+            catch (MInvalidConversionFormatException exception) {
+            }
+        }
+        if (null == y) {
+            throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x));
+        }
+        //
+        return y; // necessary to avoid Java compilation errors
+    }
+
+    public String createStringFromDate(Date x) {
+        return MDateConverter.createStringFromDate(x, this.getDefaultDateFormat(), this.getLocale(), this.getTimeZone());
+    }
+
+    /* Helpers. */
+
+    public static Date getFlatDate(Date x) {
+        if (null == x) {
+            return x;
+        }
+        //
+        Calendar calendar = new GregorianCalendar();
+        calendar.setTime(x);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        //
+        return calendar.getTime();
+    }
+
+    public static Date getCeilDate(Date x) {
+        if (null == x) {
+            return x;
+        }
+        //
+        Calendar calendar = new GregorianCalendar();
+        calendar.setTime(x);
+        calendar.set(Calendar.HOUR_OF_DAY, 23);
+        calendar.set(Calendar.MINUTE, 59);
+        calendar.set(Calendar.SECOND, 59);
+        calendar.set(Calendar.MILLISECOND, 999);
+        //
+        return calendar.getTime();
+    }
+
+}
diff --git a/src/main/java/com/marcozanon/macaco/conversions/MInvalidConversionFormatException.java b/src/main/java/com/marcozanon/macaco/conversions/MInvalidConversionFormatException.java
new file mode 100644 (file)
index 0000000..40b7d73
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Macaco
+ * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
+ * See LICENSE for details.
+ */
+
+package com.marcozanon.macaco.conversions;
+
+@SuppressWarnings("serial")
+public class MInvalidConversionFormatException extends MConversionException {
+
+    /* */
+
+    public MInvalidConversionFormatException() {
+        super();
+    }
+
+    public MInvalidConversionFormatException(String message) {
+        super(message);
+    }
+
+    public MInvalidConversionFormatException(Throwable error) {
+        super(error);
+    }
+
+    public MInvalidConversionFormatException(String message, Throwable error) {
+        super(message, error);
+    }
+
+}
diff --git a/src/main/java/com/marcozanon/macaco/conversions/MLocalDateConverter.java b/src/main/java/com/marcozanon/macaco/conversions/MLocalDateConverter.java
new file mode 100644 (file)
index 0000000..67c21e6
--- /dev/null
@@ -0,0 +1,171 @@
+/**
+ * Macaco
+ * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
+ * See LICENSE for details.
+ */
+
+package com.marcozanon.macaco.conversions;
+
+import com.marcozanon.macaco.MObject;
+import com.marcozanon.macaco.text.MText;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.format.ResolverStyle;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
+public class MLocalDateConverter extends MObject {
+
+    protected LinkedHashSet<String> dateFormats = new LinkedHashSet<String>();
+    protected Locale locale = null;
+
+    /* */
+
+    public MLocalDateConverter(String defaultDateFormat, Locale locale) {
+        super();
+        //
+        this.addDateFormat(defaultDateFormat);
+        this.setLocale(locale);
+    }
+
+    public MLocalDateConverter(LinkedHashSet<String> dateFormats, Locale locale) {
+        super();
+        //
+        this.setDateFormats(dateFormats);
+        this.setLocale(locale);
+    }
+
+    @Override
+    public MLocalDateConverter clone() {
+        return new MLocalDateConverter(this.getDateFormats(), this.getLocale());
+    }
+
+    /* Date formats. */
+
+    public void setDateFormats(LinkedHashSet<String> dateFormats) {
+        if (null == dateFormats) {
+            throw new IllegalArgumentException("Invalid 'dateFormats': null.");
+        }
+        else if (0 == dateFormats.size()) {
+            throw new IllegalArgumentException("Invalid 'dateFormats': empty.");
+        }
+        else {
+            for (String dateFormat: dateFormats) {
+                MLocalDateConverter.checkDateFormat(dateFormat);
+            }
+        }
+        //
+        synchronized (this.dateFormats) {
+            this.dateFormats = dateFormats;
+        }
+    }
+
+    public void addDateFormat(String dateFormat) {
+        MLocalDateConverter.checkDateFormat(dateFormat);
+        //
+        synchronized (this.dateFormats) {
+            this.dateFormats.add(dateFormat);
+        }
+    }
+
+    public LinkedHashSet<String> getDateFormats() {
+        return this.dateFormats;
+    }
+
+    public String getDefaultDateFormat() {
+        return this.getDateFormats().iterator().next();
+    }
+
+    public LinkedHashSet<String> getSecondaryDateFormats() {
+        LinkedHashSet<String> dateFormats = (LinkedHashSet<String>)this.getDateFormats().clone();
+        //
+        dateFormats.remove(this.getDefaultDateFormat());
+        //
+        return dateFormats;
+    }
+
+    /* Locale. */
+
+    protected void setLocale(Locale locale) {
+        if (null == locale) {
+            throw new IllegalArgumentException("Invalid 'locale': null.");
+        }
+        //
+        this.locale = locale;
+    }
+
+    public Locale getLocale() {
+        return this.locale;
+    }
+
+    /* Conversions. */
+
+    protected static void checkDateFormat(String dateFormat) {
+        if (MText.isBlank(dateFormat)) {
+            throw new IllegalArgumentException("Invalid 'dateFormat': null or empty.");
+        }
+        //
+        try {
+            DateTimeFormatter.ofPattern(dateFormat).withResolverStyle(ResolverStyle.STRICT);
+        }
+        catch (IllegalArgumentException exception) {
+            throw new IllegalArgumentException(String.format("Invalid 'dateFormat': %s.", dateFormat)); // no need to propagate exception
+        }
+    }
+
+    protected static LocalDate createDateFromString(String x, String inputDateFormat, Locale inputLocale) throws MInvalidConversionFormatException {
+        if (MText.isBlank(x)) {
+            throw new IllegalArgumentException("Invalid 'x': null or empty.");
+        }
+        MLocalDateConverter.checkDateFormat(inputDateFormat);
+        if (null == inputLocale) {
+            throw new IllegalArgumentException("Invalid 'inputLocale': null.");
+        }
+        //
+        LocalDate d = null;
+        try {
+            d = LocalDate.parse(x, DateTimeFormatter.ofPattern(inputDateFormat, inputLocale).withResolverStyle(ResolverStyle.STRICT));
+        }
+        catch (DateTimeParseException exception) {
+            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDateFormat)); // no need to propagate exception
+        }
+        //
+        return d;
+    }
+
+    protected static String createStringFromDate(LocalDate date, String outputDateFormat, Locale outputLocale) {
+        if (null == date) {
+            throw new IllegalArgumentException("Invalid 'date': null.");
+        }
+        MLocalDateConverter.checkDateFormat(outputDateFormat);
+        if (null == outputLocale) {
+            throw new IllegalArgumentException("Invalid 'outputLocale': null.");
+        }
+        //
+        return date.format(DateTimeFormatter.ofPattern(outputDateFormat, outputLocale).withResolverStyle(ResolverStyle.STRICT));
+    }
+
+    public LocalDate createDateFromString(String x) throws MInvalidConversionFormatException {
+        LocalDate y = null;
+        for (String dateFormat: this.getDateFormats()) {
+            try {
+                y = MLocalDateConverter.createDateFromString(x, dateFormat, this.getLocale());
+                //
+                return y;
+            }
+            catch (MInvalidConversionFormatException exception) {
+            }
+        }
+        if (null == y) {
+            throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x));
+        }
+        //
+        return y; // necessary to avoid Java compilation errors
+    }
+
+    public String createStringFromDate(LocalDate x) {
+        return MLocalDateConverter.createStringFromDate(x, this.getDefaultDateFormat(), this.getLocale());
+    }
+
+}
diff --git a/src/main/java/com/marcozanon/macaco/conversions/MLocalDateTimeConverter.java b/src/main/java/com/marcozanon/macaco/conversions/MLocalDateTimeConverter.java
new file mode 100644 (file)
index 0000000..4857f55
--- /dev/null
@@ -0,0 +1,171 @@
+/**
+ * Macaco
+ * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
+ * See LICENSE for details.
+ */
+
+package com.marcozanon.macaco.conversions;
+
+import com.marcozanon.macaco.MObject;
+import com.marcozanon.macaco.text.MText;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.format.ResolverStyle;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
+public class MLocalDateTimeConverter extends MObject {
+
+    protected LinkedHashSet<String> datetimeFormats = new LinkedHashSet<String>();
+    protected Locale locale = null;
+
+    /* */
+
+    public MLocalDateTimeConverter(String defaultDatetimeFormat, Locale locale) {
+        super();
+        //
+        this.addDatetimeFormat(defaultDatetimeFormat);
+        this.setLocale(locale);
+    }
+
+    public MLocalDateTimeConverter(LinkedHashSet<String> datetimeFormats, Locale locale) {
+        super();
+        //
+        this.setDatetimeFormats(datetimeFormats);
+        this.setLocale(locale);
+    }
+
+    @Override
+    public MLocalDateTimeConverter clone() {
+        return new MLocalDateTimeConverter(this.getDatetimeFormats(), this.getLocale());
+    }
+
+    /* Datetime formats. */
+
+    public void setDatetimeFormats(LinkedHashSet<String> datetimeFormats) {
+        if (null == datetimeFormats) {
+            throw new IllegalArgumentException("Invalid 'datetimeFormats': null.");
+        }
+        else if (0 == datetimeFormats.size()) {
+            throw new IllegalArgumentException("Invalid 'datetimeFormats': empty.");
+        }
+        else {
+            for (String datetimeFormat: datetimeFormats) {
+                MLocalDateTimeConverter.checkDatetimeFormat(datetimeFormat);
+            }
+        }
+        //
+        synchronized (this.datetimeFormats) {
+            this.datetimeFormats = datetimeFormats;
+        }
+    }
+
+    public void addDatetimeFormat(String datetimeFormat) {
+        MLocalDateTimeConverter.checkDatetimeFormat(datetimeFormat);
+        //
+        synchronized (this.datetimeFormats) {
+            this.datetimeFormats.add(datetimeFormat);
+        }
+    }
+
+    public LinkedHashSet<String> getDatetimeFormats() {
+        return this.datetimeFormats;
+    }
+
+    public String getDefaultDatetimeFormat() {
+        return this.getDatetimeFormats().iterator().next();
+    }
+
+    public LinkedHashSet<String> getSecondaryDatetimeFormats() {
+        LinkedHashSet<String> datetimeFormats = (LinkedHashSet<String>)this.getDatetimeFormats().clone();
+        //
+        datetimeFormats.remove(this.getDefaultDatetimeFormat());
+        //
+        return datetimeFormats;
+    }
+
+    /* Locale. */
+
+    protected void setLocale(Locale locale) {
+        if (null == locale) {
+            throw new IllegalArgumentException("Invalid 'locale': null.");
+        }
+        //
+        this.locale = locale;
+    }
+
+    public Locale getLocale() {
+        return this.locale;
+    }
+
+    /* Conversions. */
+
+    protected static void checkDatetimeFormat(String datetimeFormat) {
+        if (MText.isBlank(datetimeFormat)) {
+            throw new IllegalArgumentException("Invalid 'datetimeFormat': null or empty.");
+        }
+        //
+        try {
+            DateTimeFormatter.ofPattern(datetimeFormat).withResolverStyle(ResolverStyle.STRICT);
+        }
+        catch (IllegalArgumentException exception) {
+            throw new IllegalArgumentException(String.format("Invalid 'datetimeFormat': %s.", datetimeFormat)); // no need to propagate exception
+        }
+    }
+
+    protected static LocalDateTime createDatetimeFromString(String x, String inputDatetimeFormat, Locale inputLocale) throws MInvalidConversionFormatException {
+        if (MText.isBlank(x)) {
+            throw new IllegalArgumentException("Invalid 'x': null or empty.");
+        }
+        MLocalDateTimeConverter.checkDatetimeFormat(inputDatetimeFormat);
+        if (null == inputLocale) {
+            throw new IllegalArgumentException("Invalid 'inputLocale': null.");
+        }
+        //
+        LocalDateTime dt = null;
+        try {
+            dt = LocalDateTime.parse(x, DateTimeFormatter.ofPattern(inputDatetimeFormat, inputLocale).withResolverStyle(ResolverStyle.STRICT));
+        }
+        catch (DateTimeParseException exception) {
+            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputDatetimeFormat)); // no need to propagate exception
+        }
+        //
+        return dt;
+    }
+
+    protected static String createStringFromDatetime(LocalDateTime datetime, String outputDatetimeFormat, Locale outputLocale) {
+        if (null == datetime) {
+            throw new IllegalArgumentException("Invalid 'datetime': null.");
+        }
+        MLocalDateTimeConverter.checkDatetimeFormat(outputDatetimeFormat);
+        if (null == outputLocale) {
+            throw new IllegalArgumentException("Invalid 'outputLocale': null.");
+        }
+        //
+        return datetime.format(DateTimeFormatter.ofPattern(outputDatetimeFormat, outputLocale).withResolverStyle(ResolverStyle.STRICT));
+    }
+
+    public LocalDateTime createDatetimeFromString(String x) throws MInvalidConversionFormatException {
+        LocalDateTime y = null;
+        for (String datetimeFormat: this.getDatetimeFormats()) {
+            try {
+                y = MLocalDateTimeConverter.createDatetimeFromString(x, datetimeFormat, this.getLocale());
+                //
+                return y;
+            }
+            catch (MInvalidConversionFormatException exception) {
+            }
+        }
+        if (null == y) {
+            throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x));
+        }
+        //
+        return y; // necessary to avoid Java compilation errors
+    }
+
+    public String createStringFromDatetime(LocalDateTime x) {
+        return MLocalDateTimeConverter.createStringFromDatetime(x, this.getDefaultDatetimeFormat(), this.getLocale());
+    }
+
+}
diff --git a/src/main/java/com/marcozanon/macaco/conversions/MNumberConverter.java b/src/main/java/com/marcozanon/macaco/conversions/MNumberConverter.java
new file mode 100644 (file)
index 0000000..3e00bd3
--- /dev/null
@@ -0,0 +1,178 @@
+/**
+ * Macaco
+ * Copyright (c) 2009-2024 Marco Zanon <info@marcozanon.com>.
+ * See LICENSE for details.
+ */
+
+package com.marcozanon.macaco.conversions;
+
+import com.marcozanon.macaco.MObject;
+import com.marcozanon.macaco.text.MText;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.ParsePosition;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+
+public class MNumberConverter extends MObject {
+
+    protected LinkedHashSet<String> numberFormats = new LinkedHashSet<String>();
+    protected Locale locale = null;
+
+    /* */
+
+    public MNumberConverter(String defaultNumberFormat, Locale locale) {
+        super();
+        //
+        this.addNumberFormat(defaultNumberFormat);
+        this.setLocale(locale);
+    }
+
+    public MNumberConverter(LinkedHashSet<String> numberFormats, Locale locale) {
+        super();
+        //
+        this.setNumberFormats(numberFormats);
+        this.setLocale(locale);
+    }
+
+    @Override
+    public MNumberConverter clone() {
+        return new MNumberConverter(this.getNumberFormats(), this.getLocale());
+    }
+
+    /* Number formats. */
+
+    public void setNumberFormats(LinkedHashSet<String> numberFormats) {
+        if (null == numberFormats) {
+            throw new IllegalArgumentException("Invalid 'numberFormats': null.");
+        }
+        else if (0 == numberFormats.size()) {
+            throw new IllegalArgumentException("Invalid 'numberFormats': empty.");
+        }
+        else {
+            for (String numberFormat: numberFormats) {
+                MNumberConverter.checkNumberFormat(numberFormat);
+            }
+        }
+        //
+        synchronized (this.numberFormats) {
+            this.numberFormats = numberFormats;
+        }
+    }
+
+    public void addNumberFormat(String numberFormat) {
+        MNumberConverter.checkNumberFormat(numberFormat);
+        //
+        synchronized (this.numberFormats) {
+            this.numberFormats.add(numberFormat);
+        }
+    }
+
+    public LinkedHashSet<String> getNumberFormats() {
+        return this.numberFormats;
+    }
+
+    public String getDefaultNumberFormat() {
+        return this.getNumberFormats().iterator().next();
+    }
+
+    public LinkedHashSet<String> getSecondaryNumberFormats() {
+        LinkedHashSet<String> numberFormats = (LinkedHashSet<String>)this.getNumberFormats().clone();
+        //
+        numberFormats.remove(this.getDefaultNumberFormat());
+        //
+        return numberFormats;
+    }
+
+    /* Locale. */
+
+    protected void setLocale(Locale locale) {
+        if (null == locale) {
+            throw new IllegalArgumentException("Invalid 'locale': null.");
+        }
+        //
+        this.locale = locale;
+    }
+
+    public Locale getLocale() {
+        return this.locale;
+    }
+
+    /* Conversions. */
+
+    protected static void checkNumberFormat(String numberFormat) {
+        if (MText.isBlank(numberFormat)) {
+            throw new IllegalArgumentException("Invalid 'numberFormat': null or empty.");
+        }
+        //
+        try {
+            DecimalFormat testNumberFormat = new DecimalFormat(numberFormat);
+        }
+        catch (IllegalArgumentException exception) {
+            throw new IllegalArgumentException(String.format("Invalid 'numberFormat': %s.", numberFormat)); // no need to propagate exception
+        }
+    }
+
+    protected static BigDecimal createNumberFromString(String x, String inputNumberFormat, Locale inputLocale) throws MInvalidConversionFormatException {
+        if (MText.isBlank(x)) {
+            throw new IllegalArgumentException("Invalid 'x': null or empty.");
+        }
+        MNumberConverter.checkNumberFormat(inputNumberFormat);
+        if (null == inputLocale) {
+            throw new IllegalArgumentException("Invalid 'inputLocale': null.");
+        }
+        //
+        DecimalFormatSymbols dfs = new DecimalFormatSymbols(inputLocale);
+        DecimalFormat df = new DecimalFormat(inputNumberFormat, dfs);
+        df.setParseBigDecimal(true);
+        //
+        ParsePosition validPosition = new ParsePosition(0);
+        BigDecimal bd = (BigDecimal)df.parse(x, validPosition);
+        if (validPosition.getIndex() < x.length()) {
+            throw new MInvalidConversionFormatException(String.format("Invalid 'x' or parsing: %s (input format: %s).", x, inputNumberFormat));
+        }
+        //
+        return bd;
+    }
+
+    protected static String createStringFromNumber(BigDecimal number, String outputNumberFormat, Locale outputLocale) {
+        if (null == number) {
+            throw new IllegalArgumentException("Invalid 'number': null.");
+        }
+        MNumberConverter.checkNumberFormat(outputNumberFormat);
+        if (null == outputLocale) {
+            throw new IllegalArgumentException("Invalid 'outputLocale': null.");
+        }
+        //
+        DecimalFormatSymbols dfs = new DecimalFormatSymbols(outputLocale);
+        DecimalFormat df = new DecimalFormat(outputNumberFormat, dfs);
+        df.setRoundingMode(RoundingMode.HALF_UP);
+        //
+        return df.format(number);
+    }
+
+    public BigDecimal createNumberFromString(String x) throws MInvalidConversionFormatException {
+        BigDecimal y = null;
+        for (String numberFormat: this.getNumberFormats()) {
+            try {
+                y = MNumberConverter.createNumberFromString(x, numberFormat, this.getLocale());
+                //
+                return y;
+            }
+            catch (MInvalidConversionFormatException exception) {
+            }
+        }
+        if (null == y) {
+            throw new MInvalidConversionFormatException(String.format("Invalid 'x': %s.", x));
+        }
+        //
+        return y; // necessary to avoid Java compilation errors
+    }
+
+    public String createStringFromNumber(BigDecimal x) {
+        return MNumberConverter.createStringFromNumber(x, this.getDefaultNumberFormat(), this.getLocale());
+    }
+
+}