Skip to content

Commit ff0f154

Browse files
committed
Added mapping support to format operator (%)
1 parent 94f8a2d commit ff0f154

File tree

2 files changed

+71
-38
lines changed

2 files changed

+71
-38
lines changed

grumpy-runtime-src/runtime/str.go

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ var (
3232
StrType = newBasisType("str", reflect.TypeOf(Str{}), toStrUnsafe, BaseStringType)
3333
whitespaceSplitRegexp = regexp.MustCompile(`\s+`)
3434
strASCIISpaces = []byte(" \t\n\v\f\r")
35-
strInterpolationRegexp = regexp.MustCompile(`^%([#0 +-]?)((\*|[0-9]+)?)((\.(\*|[0-9]+))?)[hlL]?([diouxXeEfFgGcrs%])`)
35+
strInterpolationRegexp = regexp.MustCompile(`^%(\(([^)]+)\))?([#0 +-]?)(\*|[0-9]+)?(\.(\*|[0-9]+))?[hlL]?([diouxXeEfFgGcrs%])`)
3636
internedStrs = map[string]*Str{}
3737
caseOffset = byte('a' - 'A')
3838

@@ -553,18 +553,6 @@ func strLT(f *Frame, v, w *Object) (*Object, *BaseException) {
553553
return strCompare(v, w, True, False, False), nil
554554
}
555555

556-
func strMod(f *Frame, v, w *Object) (*Object, *BaseException) {
557-
s := toStrUnsafe(v).Value()
558-
switch {
559-
case w.isInstance(DictType):
560-
return nil, f.RaiseType(NotImplementedErrorType, "mappings not yet supported")
561-
case w.isInstance(TupleType):
562-
return strInterpolate(f, s, toTupleUnsafe(w))
563-
default:
564-
return strInterpolate(f, s, NewTuple1(w))
565-
}
566-
}
567-
568556
func strMul(f *Frame, v, w *Object) (*Object, *BaseException) {
569557
s := toStrUnsafe(v).Value()
570558
n, ok, raised := strRepeatCount(f, len(s), w)
@@ -1051,7 +1039,33 @@ func strCompare(v, w *Object, ltResult, eqResult, gtResult *Int) *Object {
10511039
return gtResult.ToObject()
10521040
}
10531041

1054-
func strInterpolate(f *Frame, format string, values *Tuple) (*Object, *BaseException) {
1042+
func strMod(f *Frame, v, args *Object) (*Object, *BaseException) {
1043+
format := toStrUnsafe(v).Value()
1044+
// If the format string contains mappings, args must be a dict,
1045+
// otherwise it must be treated as a tuple of values, so if it's not a tuple already
1046+
// it must be transformed into a single element tuple.
1047+
var values *Tuple
1048+
var mappings *Dict
1049+
if args.isInstance(TupleType) {
1050+
values = toTupleUnsafe(args)
1051+
} else {
1052+
values = NewTuple1(args)
1053+
if args.isInstance(DictType) {
1054+
mappings = toDictUnsafe(args)
1055+
}
1056+
}
1057+
const (
1058+
idxAll = iota
1059+
// Todo: mapping keys can contain balanced parentheses, these should be matched
1060+
// manually before using the regexp
1061+
_
1062+
idxMappingKey
1063+
idxFlags
1064+
idxWidth
1065+
idxPrecision
1066+
_
1067+
idxType
1068+
)
10551069
var buf bytes.Buffer
10561070
valueIndex := 0
10571071
index := strings.Index(format, "%")
@@ -1062,34 +1076,53 @@ func strInterpolate(f *Frame, format string, values *Tuple) (*Object, *BaseExcep
10621076
if matches == nil {
10631077
return nil, f.RaiseType(ValueErrorType, "invalid format spec")
10641078
}
1065-
flags, fieldType := matches[1], matches[7]
1066-
if fieldType != "%" && valueIndex >= len(values.elems) {
1067-
return nil, f.RaiseType(TypeErrorType, "not enough arguments for format string")
1079+
mappingKey, fieldType := matches[idxMappingKey], matches[idxType]
1080+
var value *Object
1081+
if mappingKey != "" {
1082+
// Nb: mappings are checked even in case of "%%"
1083+
if mappings == nil {
1084+
return nil, f.RaiseType(TypeErrorType, "format requires a mapping")
1085+
}
1086+
var raised *BaseException
1087+
value, raised = mappings.GetItemString(f, mappingKey)
1088+
if raised != nil {
1089+
return nil, raised
1090+
}
1091+
if value == nil {
1092+
return nil, f.RaiseType(KeyErrorType, fmt.Sprintf("'%s'", mappingKey))
1093+
}
1094+
valueIndex = 1
1095+
} else if fieldType != "%" {
1096+
if valueIndex >= len(values.elems) {
1097+
return nil, f.RaiseType(TypeErrorType, "not enough arguments for format string")
1098+
}
1099+
value = values.elems[valueIndex]
1100+
valueIndex++
10681101
}
10691102
fieldWidth := -1
1070-
if matches[2] == "*" || matches[4] != "" {
1103+
if matches[idxWidth] == "*" || matches[idxPrecision] != "" {
10711104
return nil, f.RaiseType(NotImplementedErrorType, "field width not yet supported")
10721105
}
1073-
if matches[2] != "" {
1106+
if matches[idxWidth] != "" {
10741107
var err error
1075-
fieldWidth, err = strconv.Atoi(matches[2])
1108+
fieldWidth, err = strconv.Atoi(matches[idxWidth])
10761109
if err != nil {
10771110
return nil, f.RaiseType(TypeErrorType, fmt.Sprint(err))
10781111
}
10791112
}
1113+
flags := matches[idxFlags]
10801114
if flags != "" && flags != "0" {
10811115
return nil, f.RaiseType(NotImplementedErrorType, "conversion flags not yet supported")
10821116
}
10831117
var val string
10841118
switch fieldType {
10851119
case "r", "s":
1086-
o := values.elems[valueIndex]
10871120
var s *Str
10881121
var raised *BaseException
10891122
if fieldType == "r" {
1090-
s, raised = Repr(f, o)
1123+
s, raised = Repr(f, value)
10911124
} else {
1092-
s, raised = ToStr(f, o)
1125+
s, raised = ToStr(f, value)
10931126
}
10941127
if raised != nil {
10951128
return nil, raised
@@ -1099,10 +1132,8 @@ func strInterpolate(f *Frame, format string, values *Tuple) (*Object, *BaseExcep
10991132
val = strLeftPad(val, fieldWidth, " ")
11001133
}
11011134
buf.WriteString(val)
1102-
valueIndex++
11031135
case "f":
1104-
o := values.elems[valueIndex]
1105-
if v, ok := floatCoerce(o); ok {
1136+
if v, ok := floatCoerce(value); ok {
11061137
val := strconv.FormatFloat(v, 'f', 6, 64)
11071138
if fieldWidth > 0 {
11081139
fillchar := " "
@@ -1112,13 +1143,11 @@ func strInterpolate(f *Frame, format string, values *Tuple) (*Object, *BaseExcep
11121143
val = strLeftPad(val, fieldWidth, fillchar)
11131144
}
11141145
buf.WriteString(val)
1115-
valueIndex++
11161146
} else {
1117-
return nil, f.RaiseType(TypeErrorType, fmt.Sprintf("float argument required, not %s", o.typ.Name()))
1147+
return nil, f.RaiseType(TypeErrorType, fmt.Sprintf("float argument required, not %s", value.typ.Name()))
11181148
}
11191149
case "d", "x", "X", "o":
1120-
o := values.elems[valueIndex]
1121-
i, raised := ToInt(f, values.elems[valueIndex])
1150+
i, raised := ToInt(f, value)
11221151
if raised != nil {
11231152
return nil, raised
11241153
}
@@ -1128,15 +1157,15 @@ func strInterpolate(f *Frame, format string, values *Tuple) (*Object, *BaseExcep
11281157
return nil, raised
11291158
}
11301159
val = s.Value()
1131-
} else if matches[7] == "o" {
1132-
if o.isInstance(LongType) {
1133-
val = toLongUnsafe(o).Value().Text(8)
1160+
} else if matches[idxType] == "o" {
1161+
if value.isInstance(LongType) {
1162+
val = toLongUnsafe(value).Value().Text(8)
11341163
} else {
11351164
val = strconv.FormatInt(int64(toIntUnsafe(i).Value()), 8)
11361165
}
11371166
} else {
1138-
if o.isInstance(LongType) {
1139-
val = toLongUnsafe(o).Value().Text(16)
1167+
if value.isInstance(LongType) {
1168+
val = toLongUnsafe(value).Value().Text(16)
11401169
} else {
11411170
val = strconv.FormatInt(int64(toIntUnsafe(i).Value()), 16)
11421171
}
@@ -1152,7 +1181,6 @@ func strInterpolate(f *Frame, format string, values *Tuple) (*Object, *BaseExcep
11521181
val = strLeftPad(val, fieldWidth, fillchar)
11531182
}
11541183
buf.WriteString(val)
1155-
valueIndex++
11561184
case "%":
11571185
val = "%"
11581186
if fieldWidth > 0 {
@@ -1163,7 +1191,7 @@ func strInterpolate(f *Frame, format string, values *Tuple) (*Object, *BaseExcep
11631191
format := "conversion type not yet supported: %s"
11641192
return nil, f.RaiseType(NotImplementedErrorType, fmt.Sprintf(format, fieldType))
11651193
}
1166-
format = format[len(matches[0]):]
1194+
format = format[len(matches[idxAll]):]
11671195
index = strings.Index(format, "%")
11681196
}
11691197
if valueIndex < len(values.elems) {

grumpy-runtime-src/runtime/str_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func TestStrBinaryOps(t *testing.T) {
8888
{args: wrapArgs(Mod, "%06r", "abc"), want: NewStr(" 'abc'").ToObject()},
8989
{args: wrapArgs(Mod, "%s %s", true), wantExc: mustCreateException(TypeErrorType, "not enough arguments for format string")},
9090
{args: wrapArgs(Mod, "%Z", None), wantExc: mustCreateException(ValueErrorType, "invalid format spec")},
91-
{args: wrapArgs(Mod, "%s", NewDict()), wantExc: mustCreateException(NotImplementedErrorType, "mappings not yet supported")},
91+
{args: wrapArgs(Mod, "%s", NewDict()), want: NewStr("{}").ToObject()},
9292
{args: wrapArgs(Mod, "% d", 23), wantExc: mustCreateException(NotImplementedErrorType, "conversion flags not yet supported")},
9393
{args: wrapArgs(Mod, "%.3f", 102.1), wantExc: mustCreateException(NotImplementedErrorType, "field width not yet supported")},
9494
{args: wrapArgs(Mod, "%x", 0x1f), want: NewStr("1f").ToObject()},
@@ -106,6 +106,11 @@ func TestStrBinaryOps(t *testing.T) {
106106
{args: wrapArgs(Mod, "%04o", newTestTuple(123)), want: NewStr("0173").ToObject()},
107107
{args: wrapArgs(Mod, "%o", newTestTuple("123")), wantExc: mustCreateException(TypeErrorType, "an integer is required")},
108108
{args: wrapArgs(Mod, "%o", None), wantExc: mustCreateException(TypeErrorType, "an integer is required")},
109+
{args: wrapArgs(Mod, "%(foo)s", "bar"), wantExc: mustCreateException(TypeErrorType, "format requires a mapping")},
110+
{args: wrapArgs(Mod, "%(foo)s %(bar)d", newTestDict("foo", "baz", "bar", 123)), want: NewStr("baz 123").ToObject()},
111+
{args: wrapArgs(Mod, "%(foo)s", newTestDict()), wantExc: mustCreateException(KeyErrorType, "'foo'")},
112+
{args: wrapArgs(Mod, "%(foo)s %s", newTestDict("foo", "bar")), wantExc: mustCreateException(TypeErrorType, "not enough arguments for format string")},
113+
{args: wrapArgs(Mod, "%s %(foo)s", newTestDict("foo", "bar")), want: NewStr("{'foo': 'bar'} bar").ToObject()},
109114
{args: wrapArgs(Mul, "", 10), want: NewStr("").ToObject()},
110115
{args: wrapArgs(Mul, "foo", -2), want: NewStr("").ToObject()},
111116
{args: wrapArgs(Mul, "foobar", 0), want: NewStr("").ToObject()},

0 commit comments

Comments
 (0)