Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/main/java/org/apache/hop/core/Condition.java
Original file line number Diff line number Diff line change
Expand Up @@ -847,7 +847,7 @@ public IValueMeta createValueMeta() throws HopPluginException {
IValueMeta valueMeta = ValueMetaFactory.createValueMeta(name, getHopType());
valueMeta.setLength(length, precision);
valueMeta.setConversionMask(mask);
valueMeta.setDecimalSymbol(String.valueOf(Const.DEFAULT_DECIMAL_SEPARATOR));
valueMeta.setDecimalSymbol(String.valueOf(Const.getDefaultDecimalSeparator()));
valueMeta.setGroupingSymbol(null);
valueMeta.setCurrencySymbol(null);
return valueMeta;
Expand Down
105 changes: 101 additions & 4 deletions core/src/main/java/org/apache/hop/core/Const.java
Original file line number Diff line number Diff line change
Expand Up @@ -280,22 +280,100 @@ public String getMessage() {
/** The default locale for the hop environment (system defined) */
public static final Locale DEFAULT_LOCALE = Locale.getDefault();

/** The default decimal separator . or , */
/**
* The default decimal separator . or ,
*
* @deprecated captured at class-load time from {@link #DEFAULT_LOCALE}; use {@link
* #getDefaultDecimalSeparator()} to read the active regional settings at call time.
*/
@Deprecated(since = "2.20")
public static final char DEFAULT_DECIMAL_SEPARATOR =
(new DecimalFormatSymbols(DEFAULT_LOCALE)).getDecimalSeparator();

/** The default grouping separator , or . */
/**
* The default grouping separator , or .
*
* @deprecated captured at class-load time from {@link #DEFAULT_LOCALE}; use {@link
* #getDefaultGroupingSeparator()} to read the active regional settings at call time.
*/
@Deprecated(since = "2.20")
public static final char DEFAULT_GROUPING_SEPARATOR =
(new DecimalFormatSymbols(DEFAULT_LOCALE)).getGroupingSeparator();

/** The default currency symbol */
/**
* The default currency symbol
*
* @deprecated captured at class-load time from {@link #DEFAULT_LOCALE}; use {@link
* #getDefaultCurrencySymbol()} to read the active regional settings at call time.
*/
@Deprecated(since = "2.20")
public static final String DEFAULT_CURRENCY_SYMBOL =
(new DecimalFormatSymbols(DEFAULT_LOCALE)).getCurrencySymbol();

/** The default number format */
/**
* The default number format
*
* @deprecated captured at class-load time from {@link #DEFAULT_LOCALE}; use {@link
* #getDefaultNumberFormat()} to read the active regional settings at call time.
*/
@Deprecated(since = "2.20")
public static final String DEFAULT_NUMBER_FORMAT =
((DecimalFormat) (NumberFormat.getInstance())).toPattern();

/**
* Cached symbols for the regional locale they were built from.
*
* <p>These accessors are called from the {@code ValueMetaBase} constructor, so they sit on a hot
* path: building a {@link DecimalFormatSymbols} on every call would be a real cost — the same one
* {@code ValueMetaBase.getDecimalFormat()} already warns about for {@code DecimalFormat}. The
* symbols are therefore cached and rebuilt only when the FORMAT locale actually changes.
*
* <p>Both fields are written together under {@code synchronized} and read together, so a racing
* reader can never pair one locale's symbols with another locale's marker.
*/
private static DecimalFormatSymbols cachedFormatSymbols;

private static Locale cachedFormatSymbolsLocale;

private static synchronized DecimalFormatSymbols getFormatSymbols() {
Locale formatLocale = Locale.getDefault(Locale.Category.FORMAT);
if (cachedFormatSymbols == null || !formatLocale.equals(cachedFormatSymbolsLocale)) {
cachedFormatSymbols = new DecimalFormatSymbols(formatLocale);
cachedFormatSymbolsLocale = formatLocale;
}
return cachedFormatSymbols;
}

/**
* The decimal separator of the active regional settings, read at call time.
*
* <p>Prefer this over {@link #DEFAULT_DECIMAL_SEPARATOR}, which is captured when the class is
* loaded and therefore predates the regional settings being installed.
*/
public static char getDefaultDecimalSeparator() {
return getFormatSymbols().getDecimalSeparator();
}

/** The grouping separator of the active regional settings, read at call time. */
public static char getDefaultGroupingSeparator() {
return getFormatSymbols().getGroupingSeparator();
}

/** The currency symbol of the active regional settings, read at call time. */
public static String getDefaultCurrencySymbol() {
return getFormatSymbols().getCurrencySymbol();
}

/**
* The number format pattern of the active regional settings, read at call time. In practice the
* returned pattern is locale-invariant (locale-specific separators are applied later via
* DecimalFormatSymbols), so callers do not generally need to re-read it when the locale changes.
*/
public static String getDefaultNumberFormat() {
return ((DecimalFormat) NumberFormat.getInstance(Locale.getDefault(Locale.Category.FORMAT)))
.toPattern();
}

/** Default string representing Null String values (empty) */
public static final String NULL_STRING = "";

Expand Down Expand Up @@ -838,6 +916,25 @@ public static boolean toBoolean(String string) {
public static final String HOP_AGGREGATION_ALL_NULLS_ARE_ZERO =
"HOP_AGGREGATION_ALL_NULLS_ARE_ZERO";

/**
* The FORMAT locale currently in effect (language_COUNTRY, for example {@code en_US} or {@code
* nl_BE}). Set when a lifecycle environment is enabled so pipelines can see which regional
* settings they are running under.
*/
@Variable(
description =
"The FORMAT locale in effect for number, currency and date conversion (for example en_US). Set automatically when a lifecycle environment with a format locale is enabled.")
public static final String HOP_FORMAT_LOCALE = "HOP_FORMAT_LOCALE";

/**
* The default timezone currently in effect (IANA id, for example {@code Europe/Brussels}). Set
* when a lifecycle environment is enabled.
*/
@Variable(
description =
"The default timezone in effect for date and timestamp conversion (IANA id, for example Europe/Brussels). Set automatically when a lifecycle environment with a timezone is enabled.")
public static final String HOP_TIMEZONE = "HOP_TIMEZONE";

/** The name of the variable containing an alternative default timestamp format */
@Variable(
description = "The name of the variable containing an alternative default timestamp format")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,13 @@ protected ValueMetaBase(
this.storageType = STORAGE_TYPE_NORMAL;
this.sortedDescending = false;
this.outputPaddingEnabled = false;
this.decimalSymbol = "" + Const.DEFAULT_DECIMAL_SEPARATOR;
this.groupingSymbol = "" + Const.DEFAULT_GROUPING_SEPARATOR;
this.currencySymbol = "" + Const.DEFAULT_CURRENCY_SYMBOL;
this.dateFormatLocale = Locale.getDefault();
this.decimalSymbol = "" + Const.getDefaultDecimalSeparator();
this.groupingSymbol = "" + Const.getDefaultGroupingSeparator();
this.currencySymbol = "" + Const.getDefaultCurrencySymbol();
// FORMAT, not Locale.getDefault(): the latter is the interface language once DISPLAY and
// FORMAT are split, and a field with no explicit date locale must follow the regional
// settings rather than the GUI language.
this.dateFormatLocale = Locale.getDefault(Locale.Category.FORMAT);
this.collatorDisabled = true;
this.collatorLocale = Locale.getDefault();
this.collator = Collator.getInstance(this.collatorLocale);
Expand Down Expand Up @@ -1296,7 +1299,13 @@ private synchronized SimpleDateFormat getDateFormat(int valueMetaType) {

// Do we have a locale?
//
if (dateFormatLocale == null || dateFormatLocale.equals(Locale.getDefault())) {
// Compared against the FORMAT category, not against Locale.getDefault(): that one carries the
// interface language, so a locale deliberately picked on the field would be dismissed as "no
// locale set" whenever it happened to match the language, and the field would silently follow
// the regional settings instead of the choice.
//
if (dateFormatLocale == null
|| dateFormatLocale.equals(Locale.getDefault(Locale.Category.FORMAT))) {
if (mask != null) {
dateFormat = new SimpleDateFormat(mask);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,13 @@ private synchronized SimpleDateFormat getDateFormat(int valueMetaType) {

// Do we have a locale?
//
if (dateFormatLocale == null || dateFormatLocale.equals(Locale.getDefault())) {
// Compared against the FORMAT category, not against Locale.getDefault(): that one carries the
// interface language, so a locale deliberately picked on the field would be dismissed as "no
// locale set" whenever it happened to match the language, and the field would silently follow
// the regional settings instead of the choice.
//
if (dateFormatLocale == null
|| dateFormatLocale.equals(Locale.getDefault(Locale.Category.FORMAT))) {
dateFormat = new SimpleTimestampFormat(mask);
} else {
dateFormat = new SimpleTimestampFormat(mask, dateFormatLocale);
Expand Down
Loading
Loading