From b89316fdbf5d5d4d33ab780c5ce2ab006ebb6ea7 Mon Sep 17 00:00:00 2001
From: Georg Brandl <georg@python.org>
Date: Wed, 17 May 2006 15:51:16 +0000
Subject: Patch #1180296: improve locale string formatting functions

---
 Doc/lib/liblocale.tex   |  61 ++++++++++++++---
 Lib/locale.py           | 170 +++++++++++++++++++++++++++++++++++-------------
 Lib/test/test_locale.py |  25 ++++++-
 Misc/NEWS               |   4 ++
 4 files changed, 205 insertions(+), 55 deletions(-)

diff --git a/Doc/lib/liblocale.tex b/Doc/lib/liblocale.tex
index e6ba2c1..688ccb0 100644
--- a/Doc/lib/liblocale.tex
+++ b/Doc/lib/liblocale.tex
@@ -61,7 +61,7 @@ locale.setlocale(locale.LC_ALL, '')
   Returns the database of the local conventions as a dictionary.
   This dictionary has the following strings as keys:
 
-  \begin{tableiii}{l|l|p{3in}}{constant}{Key}{Category}{Meaning}
+  \begin{tableiii}{l|l|p{3in}}{constant}{Category}{Key}{Meaning}
     \lineiii{LC_NUMERIC}{\code{'decimal_point'}}
             {Decimal point character.}
     \lineiii{}{\code{'grouping'}}
@@ -76,8 +76,20 @@ locale.setlocale(locale.LC_ALL, '')
             {International currency symbol.}
     \lineiii{}{\code{'currency_symbol'}}
             {Local currency symbol.}
+    \lineiii{}{\code{'p_cs_precedes/n_cs_precedes'}}
+            {Whether the currency symbol precedes the value (for positive resp.
+             negative values).}
+    \lineiii{}{\code{'p_sep_by_space/n_sep_by_space'}}
+            {Whether the currency symbol is separated from the value 
+             by a space (for positive resp. negative values).}
     \lineiii{}{\code{'mon_decimal_point'}}
             {Decimal point used for monetary values.}
+    \lineiii{}{\code{'frac_digits'}}
+            {Number of fractional digits used in local formatting
+             of monetary values.}
+    \lineiii{}{\code{'int_frac_digits'}}
+            {Number of fractional digits used in international
+             formatting of monetary values.}
     \lineiii{}{\code{'mon_thousands_sep'}}
             {Group separator used for monetary values.}
     \lineiii{}{\code{'mon_grouping'}}
@@ -87,13 +99,12 @@ locale.setlocale(locale.LC_ALL, '')
             {Symbol used to annotate a positive monetary value.}
     \lineiii{}{\code{'negative_sign'}}
             {Symbol used to annotate a negative monetary value.}
-    \lineiii{}{\code{'frac_digits'}}
-            {Number of fractional digits used in local formatting
-             of monetary values.}
-    \lineiii{}{\code{'int_frac_digits'}}
-            {Number of fractional digits used in international
-             formatting of monetary values.}
+    \lineiii{}{\code{'p_sign_posn/n_sign_posn'}}
+            {The position of the sign (for positive resp. negative values), see below.}
   \end{tableiii}
+  
+  All numeric values can be set to \constant{CHAR_MAX} to indicate that
+  there is no value specified in this locale.
 
   The possible values for \code{'p_sign_posn'} and
   \code{'n_sign_posn'} are given below.
@@ -104,7 +115,7 @@ locale.setlocale(locale.LC_ALL, '')
     \lineii{2}{The sign should follow the value and currency symbol.}
     \lineii{3}{The sign should immediately precede the value.}
     \lineii{4}{The sign should immediately follow the value.}
-    \lineii{\constant{LC_MAX}}{Nothing is specified in this locale.}
+    \lineii{\constant{CHAR_MAX}}{Nothing is specified in this locale.}
   \end{tableii}
 \end{funcdesc}
 
@@ -206,12 +217,44 @@ for which symbolic constants are available in the locale module.
   strings.
 \end{funcdesc}
 
-\begin{funcdesc}{format}{format, val\optional{, grouping}}
+\begin{funcdesc}{format}{format, val\optional{, grouping\optional{, monetary}}}
   Formats a number \var{val} according to the current
   \constant{LC_NUMERIC} setting.  The format follows the conventions
   of the \code{\%} operator.  For floating point values, the decimal
   point is modified if appropriate.  If \var{grouping} is true, also
   takes the grouping into account.
+
+  If \var{monetary} is true, the conversion uses monetary thousands
+  separator and grouping strings.
+
+  Please note that this function will only work for exactly one \%char
+  specifier. For whole format strings, use \function{format_string()}.
+
+  \versionchanged[Added the \var{monetary} parameter]{2.5}
+\end{funcdesc}
+
+\begin{funcdesc}{format_string}{format, val\optional{, grouping}}
+  Processes formatting specifiers as in \code{format \% val},
+  but takes the current locale settings into account.
+
+  \versionadded{2.5}
+\end{funcdesc}
+
+\begin{funcdesc}{currency}{val\optional{, symbol\optional{, grouping\optional{, international}}}}
+  Formats a number \var{val} according to the current \constant{LC_MONETARY}
+  settings. 
+  
+  The returned string includes the currency symbol if \var{symbol} is true,
+  which is the default.
+  If \var{grouping} is true (which is not the default), grouping is done with
+  the value.
+  If \var{international} is true (which is not the default), the international
+  currency symbol is used.
+
+  Note that this function will not work with the `C' locale, so you have to set
+  a locale via \function{setlocale()} first.
+
+  \versionadded{2.5}
 \end{funcdesc}
 
 \begin{funcdesc}{str}{float}
diff --git a/Lib/locale.py b/Lib/locale.py
index cfc69b1..b60c0a4 100644
--- a/Lib/locale.py
+++ b/Lib/locale.py
@@ -88,13 +88,16 @@ except ImportError:
 ### Number formatting APIs
 
 # Author: Martin von Loewis
+# improved by Georg Brandl
 
 #perform the grouping from right to left
-def _group(s):
-    conv=localeconv()
-    grouping=conv['grouping']
-    if not grouping:return (s, 0)
-    result=""
+def _group(s, monetary=False):
+    conv = localeconv()
+    thousands_sep = conv[monetary and 'mon_thousands_sep' or 'thousands_sep']
+    grouping = conv[monetary and 'mon_grouping' or 'grouping']
+    if not grouping:
+        return (s, 0)
+    result = ""
     seps = 0
     spaces = ""
     if s[-1] == ' ':
@@ -103,63 +106,142 @@ def _group(s):
         s = s[:sp]
     while s and grouping:
         # if grouping is -1, we are done
-        if grouping[0]==CHAR_MAX:
+        if grouping[0] == CHAR_MAX:
             break
         # 0: re-use last group ad infinitum
-        elif grouping[0]!=0:
+        elif grouping[0] != 0:
             #process last group
-            group=grouping[0]
-            grouping=grouping[1:]
+            group = grouping[0]
+            grouping = grouping[1:]
         if result:
-            result=s[-group:]+conv['thousands_sep']+result
+            result = s[-group:] + thousands_sep + result
             seps += 1
         else:
-            result=s[-group:]
-        s=s[:-group]
+            result = s[-group:]
+        s = s[:-group]
         if s and s[-1] not in "0123456789":
             # the leading string is only spaces and signs
-            return s+result+spaces,seps
+            return s + result + spaces, seps
     if not result:
-        return s+spaces,seps
+        return s + spaces, seps
     if s:
-        result=s+conv['thousands_sep']+result
+        result = s + thousands_sep + result
         seps += 1
-    return result+spaces,seps
-
-def format(f,val,grouping=0):
-    """Formats a value in the same way that the % formatting would use,
+    return result + spaces, seps
+
+def format(percent, value, grouping=False, monetary=False, *additional):
+    """Returns the locale-aware substitution of a %? specifier
+    (percent).
+    
+    additional is for format strings which contain one or more
+    '*' modifiers."""
+    # this is only for one-percent-specifier strings and this should be checked
+    if percent[0] != '%':
+        raise ValueError("format() must be given exactly one %char "
+                         "format specifier")
+    if additional:
+        formatted = percent % ((value,) + additional)
+    else:
+        formatted = percent % value
+    # floats and decimal ints need special action!
+    if percent[-1] in 'eEfFgG':
+        seps = 0
+        parts = formatted.split('.')
+        if grouping:
+            parts[0], seps = _group(parts[0], monetary=monetary)
+        decimal_point = localeconv()[monetary and 'mon_decimal_point'
+                                              or 'decimal_point']
+        formatted = decimal_point.join(parts)
+        while seps:
+            sp = formatted.find(' ')
+            if sp == -1: break
+            formatted = formatted[:sp] + formatted[sp+1:]
+            seps -= 1
+    elif percent[-1] in 'diu':
+        if grouping:
+            formatted = _group(formatted, monetary=monetary)[0]
+    return formatted
+
+import re, operator
+_percent_re = re.compile(r'%(?:\((?P<key>.*?)\))?'
+                         r'(?P<modifiers>[-#0-9 +*.hlL]*?)[eEfFgGdiouxXcrs%]')
+
+def format_string(f, val, grouping=False):
+    """Formats a string in the same way that the % formatting would use,
     but takes the current locale into account.
     Grouping is applied if the third parameter is true."""
-    result = f % val
-    fields = result.split(".")
-    seps = 0
-    if grouping:
-        fields[0],seps=_group(fields[0])
-    if len(fields)==2:
-        result = fields[0]+localeconv()['decimal_point']+fields[1]
-    elif len(fields)==1:
-        result = fields[0]
+    percents = list(_percent_re.finditer(f))
+    new_f = _percent_re.sub('%s', f)
+
+    if isinstance(val, tuple):
+        new_val = list(val)
+        i = 0
+        for perc in percents:
+            starcount = perc.group('modifiers').count('*')
+            new_val[i] = format(perc.group(), new_val[i], grouping, False, *new_val[i+1:i+1+starcount])
+            del new_val[i+1:i+1+starcount]
+            i += (1 + starcount)
+        val = tuple(new_val)
+    elif operator.isMappingType(val):
+        for perc in percents:
+            key = perc.group("key")
+            val[key] = format(perc.group(), val[key], grouping)
     else:
-        raise Error, "Too many decimal points in result string"
+        # val is a single value
+        val = format(percents[0].group(), val, grouping)
+
+    return new_f % val
+
+def currency(val, symbol=True, grouping=False, international=False):
+    """Formats val according to the currency settings
+    in the current locale."""
+    conv = localeconv()
 
-    while seps:
-        # If the number was formatted for a specific width, then it
-        # might have been filled with spaces to the left or right. If
-        # so, kill as much spaces as there where separators.
-        # Leading zeroes as fillers are not yet dealt with, as it is
-        # not clear how they should interact with grouping.
-        sp = result.find(" ")
-        if sp==-1:break
-        result = result[:sp]+result[sp+1:]
-        seps -= 1
+    # check for illegal values
+    digits = conv[international and 'int_frac_digits' or 'frac_digits']
+    if digits == 127:
+        raise ValueError("Currency formatting is not possible using "
+                         "the 'C' locale.")
+
+    s = format('%%.%if' % digits, abs(val), grouping, monetary=True)
+    # '<' and '>' are markers if the sign must be inserted between symbol and value
+    s = '<' + s + '>'
+
+    if symbol:
+        smb = conv[international and 'int_curr_symbol' or 'currency_symbol']
+        precedes = conv[val<0 and 'n_cs_precedes' or 'p_cs_precedes']
+        separated = conv[val<0 and 'n_sep_by_space' or 'p_sep_by_space']
+
+        if precedes:
+            s = smb + (separated and ' ' or '') + s
+        else:
+            s = s + (separated and ' ' or '') + smb
+
+    sign_pos = conv[val<0 and 'n_sign_posn' or 'p_sign_posn']
+    sign = conv[val<0 and 'negative_sign' or 'positive_sign']
+
+    if sign_pos == 0:
+        s = '(' + s + ')'
+    elif sign_pos == 1:
+        s = sign + s
+    elif sign_pos == 2:
+        s = s + sign
+    elif sign_pos == 3:
+        s = s.replace('<', sign)
+    elif sign_pos == 4:
+        s = s.replace('>', sign)
+    else:
+        # the default if nothing specified;
+        # this should be the most fitting sign position
+        s = sign + s
 
-    return result
+    return s.replace('<', '').replace('>', '')
 
 def str(val):
     """Convert float to integer, taking the locale into account."""
-    return format("%.12g",val)
+    return format("%.12g", val)
 
-def atof(string,func=float):
+def atof(string, func=float):
     "Parses a string as a float according to the locale settings."
     #First, get rid of the grouping
     ts = localeconv()['thousands_sep']
@@ -179,10 +261,10 @@ def atoi(str):
 def _test():
     setlocale(LC_ALL, "")
     #do grouping
-    s1=format("%d", 123456789,1)
+    s1 = format("%d", 123456789,1)
     print s1, "is", atoi(s1)
     #standard formatting
-    s1=str(3.14)
+    s1 = str(3.14)
     print s1, "is", atof(s1)
 
 ### Locale name aliasing engine
diff --git a/Lib/test/test_locale.py b/Lib/test/test_locale.py
index 1523e77..ec5a533 100644
--- a/Lib/test/test_locale.py
+++ b/Lib/test/test_locale.py
@@ -20,14 +20,14 @@ for tloc in tlocs:
 else:
     raise ImportError, "test locale not supported (tried %s)"%(', '.join(tlocs))
 
-def testformat(formatstr, value, grouping = 0, output=None):
+def testformat(formatstr, value, grouping = 0, output=None, func=locale.format):
     if verbose:
         if output:
             print "%s %% %s =? %s ..." %\
                 (repr(formatstr), repr(value), repr(output)),
         else:
             print "%s %% %s works? ..." % (repr(formatstr), repr(value)),
-    result = locale.format(formatstr, value, grouping = grouping)
+    result = func(formatstr, value, grouping = grouping)
     if output and result != output:
         if verbose:
             print 'no'
@@ -49,6 +49,27 @@ try:
     testformat("%-10.f", 4200, grouping=1, output='4%s200     ' % sep)
     # Invoke getpreferredencoding to make sure it does not cause exceptions,
     locale.getpreferredencoding()
+
+    # === Test format() with more complex formatting strings
+    # test if grouping is independent from other characters in formatting string
+    testformat("One million is %i", 1000000, grouping=1, output='One million is 1,000,000',
+               func=locale.format_string)
+    testformat("One  million is %i", 1000000, grouping=1, output='One  million is 1,000,000',
+               func=locale.format_string)
+    # test dots in formatting string
+    testformat(".%f.", 1000.0, output='.1000.000000.', func=locale.format_string)
+    # test floats
+    testformat("--> %10.2f", 1000.0, grouping=1, output='-->   1,000.00',
+               func=locale.format_string)
+    # test asterisk formats
+    testformat("%10.*f", (2, 1000.0), grouping=0, output='   1000.00',
+               func=locale.format_string)
+    testformat("%*.*f", (10, 2, 1000.0), grouping=1, output='  1,000.00',
+               func=locale.format_string)
+    # test more-in-one
+    testformat("int %i float %.2f str %s", (1000, 1000.0, 'str'), grouping=1,
+               output='int 1,000 float 1,000.00 str str', func=locale.format_string)
+
 finally:
     locale.setlocale(locale.LC_NUMERIC, oldlocale)
 
diff --git a/Misc/NEWS b/Misc/NEWS
index 514c95d..99dcec8 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -45,6 +45,10 @@ Extension Modules
 Library
 -------
 
+- Patch #1180296: Two new functions were added to the locale module:
+  format_string() to get the effect of  "format % items" but locale-aware,
+  and currency() to format a monetary number with currency sign.
+
 - Patch #1486962: Several bugs in the turtle Tk demo module were fixed
   and several features added, such as speed and geometry control.
 
-- 
cgit v0.12

