Skip to content

Commit bcd87cb

Browse files
authored
Add underscore formatting for types dxob (#1513)
* Add underscore formatting for type d * Add underscore formatting for types xob
1 parent e6690c2 commit bcd87cb

7 files changed

Lines changed: 203 additions & 128 deletions

File tree

Src/IronPython/Runtime/FormattingHelper.cs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5+
#nullable enable
6+
57
using System;
8+
using System.Diagnostics;
69
using System.Globalization;
710
using System.Text;
811
using System.Threading;
912

1013
namespace IronPython.Runtime {
1114
internal static class FormattingHelper {
12-
private static NumberFormatInfo _invariantUnderscoreSeperatorInfo;
15+
private static NumberFormatInfo? _invariantUnderscoreSeperatorInfo;
1316

1417
/// <summary>
1518
/// Helper NumberFormatInfo for use by int/BigInteger __format__ routines
@@ -33,10 +36,10 @@ public static NumberFormatInfo InvariantUnderscoreNumberInfo {
3336
}
3437
}
3538

36-
public static string/*!*/ ToCultureString<T>(T/*!*/ val, NumberFormatInfo/*!*/ nfi, StringFormatSpec spec, int? overrideWidth = null) {
39+
public static string/*!*/ ToCultureString<T>(T/*!*/ val, NumberFormatInfo/*!*/ nfi, StringFormatSpec spec, int? overrideWidth = null) where T : notnull {
3740
string separator = nfi.NumberGroupSeparator;
3841
int[] separatorLocations = nfi.NumberGroupSizes;
39-
string digits = val.ToString();
42+
string digits = val.ToString()!;
4043

4144
// If we're adding leading zeros, we need to know how
4245
// many we need.
@@ -126,5 +129,58 @@ public static NumberFormatInfo InvariantUnderscoreNumberInfo {
126129

127130
return digits;
128131
}
132+
133+
public static string AddUnderscores(string digits, StringFormatSpec spec, bool isNegative) {
134+
var length = digits.Length + (digits.Length - 1) / 4; // length including minimum number of underscores
135+
136+
int idx;
137+
var fillLength = 0;
138+
if (spec.Fill == '0') {
139+
if (spec.Width > length) {
140+
var width = spec.Width.Value;
141+
if (isNegative || spec.Sign != null && spec.Sign != '-') width--;
142+
fillLength = width - length;
143+
length = width;
144+
}
145+
146+
// index of first underscore
147+
idx = length % 5;
148+
if (idx == 0) {
149+
idx = 1;
150+
fillLength++;
151+
length++;
152+
}
153+
} else {
154+
// index of first underscore
155+
idx = length % 5;
156+
if (idx == 0) {
157+
idx = 1;
158+
length++;
159+
}
160+
}
161+
162+
var sb = new StringBuilder(length);
163+
164+
for (int i = 0; i < fillLength; i++, idx--) {
165+
if (idx == 0) {
166+
sb.Append('_');
167+
idx = 5;
168+
} else {
169+
sb.Append('0');
170+
}
171+
}
172+
int j = 0;
173+
for (int i = fillLength; i < length; i++, idx--) {
174+
if (idx == 0) {
175+
sb.Append('_');
176+
idx = 5;
177+
} else {
178+
sb.Append(digits[j++]);
179+
}
180+
}
181+
Debug.Assert(j == digits.Length);
182+
183+
return sb.ToString();
184+
}
129185
}
130186
}

Src/IronPython/Runtime/Operations/BigIntegerOps.cs

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -758,8 +758,7 @@ public static BigInteger ToBigInteger(BigInteger self) {
758758

759759
if (spec.Fill == '0' && spec.Width > 1) {
760760
digits = FormattingHelper.ToCultureString(val, culture.NumberFormat, spec, (spec.Sign != null && spec.Sign != '-' || self < 0) ? spec.Width - 1 : null);
761-
}
762-
else {
761+
} else {
763762
digits = FormattingHelper.ToCultureString(val, culture.NumberFormat, spec);
764763
}
765764
break;
@@ -790,24 +789,36 @@ public static BigInteger ToBigInteger(BigInteger self) {
790789
digits = DoubleOps.DoubleToFormatString(context, ToDouble(val), spec);
791790
break;
792791
case 'X':
793-
digits = AbsToHex(val, lowercase: false);
792+
digits = ToHexDigits(val, lowercase: false);
793+
if (spec.ThousandsUnderscore) {
794+
digits = FormattingHelper.AddUnderscores(digits, spec, self.IsNegative());
795+
}
794796
break;
795797
case 'x':
796-
digits = AbsToHex(val, lowercase: true);
798+
digits = ToHexDigits(val, lowercase: true);
799+
if (spec.ThousandsUnderscore) {
800+
digits = FormattingHelper.AddUnderscores(digits, spec, self.IsNegative());
801+
}
797802
break;
798803
case 'o': // octal
799-
digits = ToOctal(val, lowercase: true);
804+
digits = ToOctalDigits(val);
805+
if (spec.ThousandsUnderscore) {
806+
digits = FormattingHelper.AddUnderscores(digits, spec, self.IsNegative());
807+
}
800808
break;
801809
case 'b': // binary
802-
digits = ToBinary(val, includeType: false, lowercase: true);
810+
digits = ToBinaryDigits(val);
811+
if (spec.ThousandsUnderscore) {
812+
digits = FormattingHelper.AddUnderscores(digits, spec, self.IsNegative());
813+
}
803814
break;
804815
case 'c': // single char
805816
int iVal;
806817
if (spec.Sign != null) {
807818
throw PythonOps.ValueError("Sign not allowed with integer format specifier 'c'");
808819
} else if (!self.AsInt32(out iVal)) {
809820
throw PythonOps.OverflowError("Python int too large to convert to System.Int32");
810-
} else if(iVal < 0 || iVal > 0x10ffff) {
821+
} else if (iVal < 0 || iVal > 0x10ffff) {
811822
throw PythonOps.OverflowError("%c arg not in range(0x110000)");
812823
}
813824

@@ -1025,8 +1036,10 @@ public static TypeCode GetTypeCode(BigInteger self) {
10251036

10261037
#region Helpers
10271038

1039+
/// <summary>
1040+
/// Unlike ConvertToDouble, this method produces a Python-specific overflow error messge.
1041+
/// </summary>
10281042
internal static double ToDouble(BigInteger self) {
1029-
// Unlike ConvertToDouble, this method produces a Python-specific overflow error messge.
10301043
if (MathUtils.TryToFloat64(self, out double res)) {
10311044
return res;
10321045
}
@@ -1037,27 +1050,24 @@ internal static string AbsToHex(BigInteger val, bool lowercase) {
10371050
return ToDigits(val, 16, lowercase);
10381051
}
10391052

1040-
private static string ToOctal(BigInteger val, bool lowercase) {
1041-
return ToDigits(val, 8, lowercase);
1053+
private static string ToHexDigits(BigInteger val, bool lowercase) {
1054+
Debug.Assert(val >= 0);
1055+
return ToDigits(val, 16, lower: lowercase);
10421056
}
10431057

1044-
internal static string ToBinary(BigInteger val) {
1045-
string res = ToBinary(BigInteger.Abs(val), true, true);
1046-
if (val.IsNegative()) {
1047-
res = "-" + res;
1048-
}
1049-
return res;
1058+
private static string ToOctalDigits(BigInteger val) {
1059+
Debug.Assert(val >= 0);
1060+
return ToDigits(val, 8, lower: false);
10501061
}
10511062

1052-
private static string ToBinary(BigInteger val, bool includeType, bool lowercase) {
1053-
Debug.Assert(!val.IsNegative());
1054-
1055-
string digits = ToDigits(val, 2, lowercase);
1063+
private static string ToBinaryDigits(BigInteger val) {
1064+
Debug.Assert(val >= 0);
1065+
return ToDigits(val, 2, lower: false);
1066+
}
10561067

1057-
if (includeType) {
1058-
digits = (lowercase ? "0b" : "0B") + digits;
1059-
}
1060-
return digits;
1068+
internal static string ToBinary(BigInteger val) {
1069+
var digits = ToBinaryDigits(BigInteger.Abs(val));
1070+
return ((val < 0) ? "-0b" : "0b") + digits;
10611071
}
10621072

10631073
private static string/*!*/ ToDigits(BigInteger/*!*/ val, int radix, bool lower) {
@@ -1066,12 +1076,12 @@ private static string ToBinary(BigInteger val, bool includeType, bool lowercase)
10661076
}
10671077

10681078
StringBuilder str = new StringBuilder();
1079+
char a = lower ? 'a' : 'A';
10691080

10701081
while (val != 0) {
10711082
int digit = (int)(val % radix);
10721083
if (digit < 10) str.Append((char)((digit) + '0'));
1073-
else if (lower) str.Append((char)((digit - 10) + 'a'));
1074-
else str.Append((char)((digit - 10) + 'A'));
1084+
else str.Append((char)((digit - 10) + a));
10751085

10761086
val /= radix;
10771087
}

Src/IronPython/Runtime/Operations/IntOps.cs

Lines changed: 44 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -262,16 +262,28 @@ public static string __format__(CodeContext/*!*/ context, int self, [NotNone] st
262262
digits = DoubleOps.DoubleToFormatString(context, val, spec);
263263
break;
264264
case 'X':
265-
digits = ToHex(val, lowercase: false);
265+
digits = ToHexDigits(val, lowercase: false);
266+
if (spec.ThousandsUnderscore) {
267+
digits = FormattingHelper.AddUnderscores(digits, spec, self < 0);
268+
}
266269
break;
267270
case 'x':
268-
digits = ToHex(val, lowercase: true);
271+
digits = ToHexDigits(val, lowercase: true);
272+
if (spec.ThousandsUnderscore) {
273+
digits = FormattingHelper.AddUnderscores(digits, spec, self < 0);
274+
}
269275
break;
270276
case 'o': // octal
271-
digits = ToOctal(val, lowercase: true);
277+
digits = ToOctalDigits(val);
278+
if (spec.ThousandsUnderscore) {
279+
digits = FormattingHelper.AddUnderscores(digits, spec, self < 0);
280+
}
272281
break;
273282
case 'b': // binary
274-
digits = ToBinary(val, includeType: false);
283+
digits = ToBinaryDigits(val) ;
284+
if (spec.ThousandsUnderscore) {
285+
digits = FormattingHelper.AddUnderscores(digits, spec, self < 0);
286+
}
275287
break;
276288
case 'c': // single char
277289
if (spec.Sign != null) {
@@ -306,86 +318,47 @@ public static object from_bytes(CodeContext context, PythonType type, object byt
306318

307319
#region Helpers
308320

309-
private static string ToHex(int self, bool lowercase) {
310-
string digits;
311-
if (self != Int32.MinValue) {
312-
int val = self;
313-
if (self < 0) {
314-
val = -self;
315-
}
316-
digits = val.ToString(lowercase ? "x" : "X", CultureInfo.InvariantCulture);
317-
} else {
318-
digits = "80000000";
319-
}
320-
321-
return digits;
321+
private static string ToHexDigits(int val, bool lowercase) {
322+
Debug.Assert(val >= 0);
323+
return val.ToString(lowercase ? "x" : "X", CultureInfo.InvariantCulture);
322324
}
323325

324-
private static string ToOctal(int self, bool lowercase) {
325-
string digits;
326-
if (self == 0) {
327-
digits = "0";
328-
} else if (self != Int32.MinValue) {
329-
int val = self;
330-
if (self < 0) {
331-
val = -self;
332-
}
326+
private static string ToOctalDigits(int val) {
327+
Debug.Assert(val >= 0);
328+
if (val == 0) return "0";
333329

334-
StringBuilder sbo = new StringBuilder();
335-
for (int i = 30; i >= 0; i -= 3) {
336-
char value = (char)('0' + (val >> i & 0x07));
337-
if (value != '0' || sbo.Length > 0) {
338-
sbo.Append(value);
339-
}
330+
StringBuilder sb = new StringBuilder();
331+
for (int i = 30; i >= 0; i -= 3) {
332+
char value = (char)('0' + (val >> i & 0x07));
333+
if (value != '0' || sb.Length > 0) {
334+
sb.Append(value);
340335
}
341-
digits = sbo.ToString();
342-
} else {
343-
digits = "20000000000";
344336
}
345-
346-
return digits;
337+
return sb.ToString();
347338
}
348339

349-
internal static string ToBinary(int self) {
350-
if (self == Int32.MinValue) {
351-
return "-0b10000000000000000000000000000000";
352-
}
340+
private static string ToBinaryDigits(int val) {
341+
Debug.Assert(val >= 0);
342+
if (val == 0) return "0";
353343

354-
string res = ToBinary(self, true);
355-
if (self < 0) {
356-
res = "-" + res;
344+
StringBuilder sb = new StringBuilder();
345+
for (int i = 31; i >= 0; i--) {
346+
if ((val & (1 << i)) != 0) {
347+
sb.Append('1');
348+
} else if (sb.Length != 0) {
349+
sb.Append('0');
350+
}
357351
}
358-
return res;
352+
return sb.ToString();
359353
}
360354

361-
private static string ToBinary(int self, bool includeType) {
362-
string digits;
363-
if (self == 0) {
364-
digits = "0";
365-
} else if (self != Int32.MinValue) {
366-
StringBuilder sbb = new StringBuilder();
367-
368-
int val = self;
369-
if (self < 0) {
370-
val = -self;
371-
}
372-
373-
for (int i = 31; i >= 0; i--) {
374-
if ((val & (1 << i)) != 0) {
375-
sbb.Append('1');
376-
} else if (sbb.Length != 0) {
377-
sbb.Append('0');
378-
}
379-
}
380-
digits = sbb.ToString();
381-
} else {
382-
digits = "10000000000000000000000000000000";
355+
internal static string ToBinary(int val) {
356+
if (val == int.MinValue) {
357+
return "-0b10000000000000000000000000000000";
383358
}
384359

385-
if (includeType) {
386-
digits = "0b" + digits;
387-
}
388-
return digits;
360+
var digits = ToBinaryDigits(Math.Abs(val));
361+
return ((val < 0) ? "-0b" : "0b") + digits;
389362
}
390363

391364
#endregion

Src/IronPython/Runtime/StringFormatSpec.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,15 @@ private StringFormatSpec(char? fill, char? alignment, char? sign, int? width, bo
131131
curOffset++;
132132
}
133133

134-
// TODO: read optional underscore (new in 3.6)
134+
// read optional underscore
135+
if (curOffset != formatSpec.Length &&
136+
formatSpec[curOffset] == '_') {
137+
thousandsUnderscore = true;
138+
curOffset++;
139+
if (thousandsComma || curOffset != formatSpec.Length && formatSpec[curOffset] == ',') {
140+
throw PythonOps.ValueError("Cannot specify both ',' and '_'");
141+
}
142+
}
135143

136144
// read precision
137145
if (curOffset != formatSpec.Length &&
@@ -191,11 +199,14 @@ private StringFormatSpec(char? fill, char? alignment, char? sign, int? width, bo
191199
break;
192200
default:
193201
throw PythonOps.ValueError("Cannot specify '_' with '{0}'", type);
194-
195202
}
196203
}
197204
}
198205

206+
if (curOffset != formatSpec.Length) {
207+
throw PythonOps.ValueError("Invalid format specifier '{0}'", formatSpec);
208+
}
209+
199210
return new StringFormatSpec(
200211
fill,
201212
align,

0 commit comments

Comments
 (0)