Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
package grails.plugin.formfields

import java.sql.Blob
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.text.NumberFormat
import java.time.Instant
import java.time.LocalDate
Expand Down Expand Up @@ -833,7 +835,15 @@ class FormFieldsTagLib {

@CompileStatic
protected NumberFormat getNumberFormatter() {
NumberFormat.getInstance(getLocale())
NumberFormat numberFormat = NumberFormat.getInstance(getLocale())
// Normalize Unicode minus sign (U+2212) to ASCII hyphen-minus (U+002D)
// for HTML compatibility (fixes grails-core#15178)
if (numberFormat instanceof DecimalFormat) {
DecimalFormatSymbols symbols = ((DecimalFormat) numberFormat).decimalFormatSymbols
symbols.minusSign = '-' as char
((DecimalFormat) numberFormat).decimalFormatSymbols = symbols
}
return numberFormat
}

@CompileStatic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,13 +327,25 @@ class FormatTagLib implements TagLibrary {
number = decimalFormat.parse(number as String)
}

// Normalize Unicode minus sign (U+2212) to ASCII hyphen-minus (U+002D)
// for HTML compatibility (fixes grails-core#15178)
DecimalFormatSymbols formatSymbols = decimalFormat.decimalFormatSymbols
formatSymbols.minusSign = '-' as char
Comment thread
jamesfredley marked this conversation as resolved.
Outdated
decimalFormat.decimalFormatSymbols = formatSymbols

def formatted
try {
formatted = decimalFormat.format(number)
}
catch (ArithmeticException e) {
// if roundingMode is UNNECESSARY and ArithemeticException raises, just return original number formatted with default number formatting
formatted = NumberFormat.getNumberInstance(locale).format(number)
NumberFormat fallbackFormat = NumberFormat.getNumberInstance(locale)
if (fallbackFormat instanceof DecimalFormat) {
DecimalFormatSymbols fallbackSymbols = ((DecimalFormat) fallbackFormat).decimalFormatSymbols
fallbackSymbols.minusSign = '-' as char
((DecimalFormat) fallbackFormat).decimalFormatSymbols = fallbackSymbols
}
formatted = fallbackFormat.format(number)
}
return formatted
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,12 @@ class ValidationTagLib implements TagLibrary {
PropertyEditor editor = registry.findCustomEditor(value.getClass(), propertyPath)
if (editor) {
editor.setValue(value)
return !(value instanceof Number) ? editor.asText?.encodeAsHTML() : editor.asText
String formattedValue = editor.asText
if (value instanceof Number && formattedValue != null) {
DecimalFormatSymbols formatSymbols = new DecimalFormatSymbols(webRequest.getLocale())
formattedValue = formattedValue.replace(formatSymbols.minusSign, '-' as char)
}
return !(value instanceof Number) ? formattedValue?.encodeAsHTML() : formattedValue
Comment thread
jamesfredley marked this conversation as resolved.
Outdated
}

if (value instanceof Number) {
Expand All @@ -482,6 +487,7 @@ class ValidationTagLib implements TagLibrary {

def locale = webRequest.getLocale()
def dcfs = locale ? new DecimalFormatSymbols(locale) : new DecimalFormatSymbols()
dcfs.minusSign = '-' as char
def decimalFormat = new DecimalFormat(pattern, dcfs)
value = decimalFormat.format(value)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package org.grails.web.taglib
import grails.testing.web.taglib.TagLibUnitTest
import org.grails.plugins.web.taglib.FormatTagLib
import spock.lang.IgnoreIf
import spock.lang.Issue
import spock.lang.Requires
import spock.lang.Specification

Expand Down Expand Up @@ -57,4 +58,14 @@ class FormatTagLibSpec extends Specification implements TagLibUnitTest<FormatTag
expect:
"3,12${new String([160] as char[])}\$" == applyTemplate('<g:formatNumber type="currency" currencyCode="USD" number="${number}" locale="fi_FI" />', [number: number])
}

@Issue('https://github.com/apache/grails-core/issues/15178')
void "formatNumber uses ASCII minus sign for negative numbers with Norwegian locale"() {
when:
def result = applyTemplate('<g:formatNumber number="${n}" type="number" locale="nb_NO"/>', [n: -42])

then: "ASCII minus (U+002D) is used, not Unicode minus (U+2212)"
result.contains('-')
!result.contains('\u2212')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,32 @@ class FormatTagLibTests extends AbstractGrailsTagTests {
def template = '<g:formatNumber number="${number}" nan="n/a"/>'
assertOutputEquals("n/a", template, [number: number])
}

@Issue('https://github.com/apache/grails-core/issues/15178')
@Test
void testFormatNumberNegativeWithNorwegianLocale() {
def template = '<g:formatNumber number="${myNumber}" type="number" locale="nb_NO"/>'
def result = applyTemplate(template, [myNumber: -42])
// Verify ASCII minus (U+002D) is used, not Unicode minus (U+2212)
assert result.charAt(0) == '-' as char
assert !result.contains('\u2212')
}

@Issue('https://github.com/apache/grails-core/issues/15178')
@Test
void testFormatNumberNegativeLongWithNorwegianLocale() {
def template = '<g:formatNumber number="${myNumber}" format="0" locale="nb_NO"/>'
def result = applyTemplate(template, [myNumber: -123456L])
assert result.contains('-')
assert !result.contains('\u2212')
}

@Issue('https://github.com/apache/grails-core/issues/15178')
@Test
void testFormatNumberNegativeBigDecimalWithNorwegianLocale() {
def template = '<g:formatNumber number="${myNumber}" format="0.00" locale="nb_NO"/>'
def result = applyTemplate(template, [myNumber: new BigDecimal("-99.95")])
assert result.contains('-')
assert !result.contains('\u2212')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition
import org.springframework.context.MessageSourceResolvable
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.validation.FieldError
import spock.lang.Issue
import spock.lang.PendingFeature
import spock.lang.Specification

Expand Down Expand Up @@ -259,6 +260,22 @@ class ValidationTagLibSpec extends Specification implements TagLibUnitTest<Valid
applyTemplate(template, [book:b]) == "1.045,456"
}

@Issue('https://github.com/apache/grails-core/issues/15178')
void testFieldValueTagWithNorwegianLocaleUsesAsciiMinus() {
given:
def b = new ValidationTagLibBook()
b.usPrice = -42.5
def template = '<g:fieldValue bean="${book}" field="usPrice" />'

when:
webRequest.currentRequest.addPreferredLocale(new Locale('nb', 'NO'))
def result = applyTemplate(template, [book: b])

then:
result.contains('-')
!result.contains('\u2212')
}

@PendingFeature // Was valid for JVM lower than 14 because space is converted to narrow no-break space 8239 and is not encoded
void testFieldValueTagWithFrenchLocaleInTextField() {
given:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ class TagLibController {
]
}

def formatLocaleNumber() {
[
negativeInt: -42,
negativeLong: -123456789L,
negativeBigDecimal: new BigDecimal('-99999.99')
]
}

def setTag() {
render(view: 'setTag')
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<%--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you 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
~
~ https://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.
--%>
<!DOCTYPE html>
<html>
<head>
<title>Format Locale Number Test</title>
</head>
<body>
<h1>Format Locale Number Test</h1>
<span id="neg-int-no"><g:formatNumber number="${negativeInt}" locale="nb_NO"/></span>
<span id="neg-long-no"><g:formatNumber number="${negativeLong}" locale="nb_NO" format="#,##0"/></span>
<span id="neg-decimal-no"><g:formatNumber number="${negativeBigDecimal}" locale="nb_NO" format="#,##0.00"/></span>
<span id="pos-number-no"><g:formatNumber number="${42}" locale="nb_NO"/></span>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,44 @@ class GspTagLibSpec extends ContainerGebSpec {
$('#boolean-display').text().contains('Yes')
}

def "g:formatNumber with Norwegian locale uses ASCII minus for negative int"() {
when:
go('tagLib/formatLocaleNumber')

then:
def text = $('#neg-int-no').text()
text.contains('-')
!text.contains('\u2212')
}

def "g:formatNumber with Norwegian locale uses ASCII minus for negative long"() {
when:
go('tagLib/formatLocaleNumber')

then:
def text = $('#neg-long-no').text()
text.contains('-')
!text.contains('\u2212')
}

def "g:formatNumber with Norwegian locale uses ASCII minus for negative BigDecimal"() {
when:
go('tagLib/formatLocaleNumber')

then:
def text = $('#neg-decimal-no').text()
text.contains('-')
!text.contains('\u2212')
}

def "g:formatNumber with Norwegian locale formats positive numbers correctly"() {
when:
go('tagLib/formatLocaleNumber')

then:
$('#pos-number-no').text() == '42'
}

def "g:set creates local variable"() {
when:
go('tagLib/setTag')
Expand Down Expand Up @@ -272,4 +310,42 @@ class GspTagLibSpec extends ContainerGebSpec {
$('#url-encoded').text().contains('%3D') || // encoded =
$('#url-encoded').text().contains('param') // at least the content is there
}

def "g:formatNumber with Norwegian locale uses ASCII minus for negative int"() {
when:
go('tagLib/formatLocaleNumber')

then:
def text = $('#neg-int-no').text()
text.contains('-')
!text.contains('\u2212')
}

def "g:formatNumber with Norwegian locale uses ASCII minus for negative long"() {
when:
go('tagLib/formatLocaleNumber')

then:
def text = $('#neg-long-no').text()
text.contains('-')
!text.contains('\u2212')
}

def "g:formatNumber with Norwegian locale uses ASCII minus for negative BigDecimal"() {
when:
go('tagLib/formatLocaleNumber')

then:
def text = $('#neg-decimal-no').text()
text.contains('-')
!text.contains('\u2212')
}

def "g:formatNumber with Norwegian locale formats positive numbers correctly"() {
when:
go('tagLib/formatLocaleNumber')

then:
$('#pos-number-no').text() == '42'
}
}
Loading