Skip to content

Commit aeb4a34

Browse files
authored
feat: comp-3 decimal separator localization (#40)
* feat: comp-3 decimal separator handling * style: lint
1 parent 98e890b commit aeb4a34

File tree

10 files changed

+112
-22
lines changed

10 files changed

+112
-22
lines changed

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@ Types of changes
1414
- `Fixed` for any bug fixes.
1515
- `Security` in case of vulnerabilities.
1616

17+
## [0.3.0]
18+
19+
- `Added` COMP-3 codec now accepts both `.` and `,` as decimal separators from JSON input.
20+
- `Added` `usecomma` flag: when enabled, the COMP-3 codec encodes the decimal separator as `,` in JSON output.
21+
1722
## [0.2.0]
1823

19-
- `Added` comp-3 numeric encoding support
20-
- `Fixed` always validate schema and automatically create missing fillers as non exported fields
24+
- `Added` COMP-3 numeric encoding support.
25+
- `Fixed` always validate schema and automatically create missing fillers as non exported fields.
2126

2227
## [0.1.0]
2328

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,10 @@ $ posimap fold --notrim < person.fixed-width
170170

171171
### Packed decimal (COMP-3)
172172

173-
POSIMAP supports packed decimal (COMP-3) encoding, commonly used in mainframe systems. When working with packed decimal fields, you need to:
173+
POSIMAP supports packed decimal (COMP-3) encoding, commonly used in mainframe systems.
174+
Packed decimal fields typically use half the bytes of their displayed value, plus half a byte for the sign.
175+
176+
When working with packed decimal fields, you need to:
174177

175178
1. Specify the `codec: COMP-3` attribute
176179
2. Define the `picture` clause that describes the field format
@@ -189,7 +192,8 @@ The `picture` format follows standard COBOL notation:
189192
- `9(n)` represents n numeric digits
190193
- `V` represents an implied decimal point (no actual character in the data)
191194

192-
Packed decimal fields typically use half the bytes of their displayed value, plus half a byte for the sign.
195+
The COMP-3 codec accepts both `.` and `,` as decimal separators from JSON input and outputs JSON using `.` as the default decimal separator.
196+
When the `usecomma` flag is enabled, the COMP-3 codec will encode the decimal separator as `,` in JSON output.
193197

194198
## Contributing
195199

internal/appli/command/fold.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Fold struct {
3838
configfile string
3939
notrim bool
4040
charset string
41+
usecomma bool
4142
}
4243

4344
func NewFoldCommand(rootname string, groupid string) *cobra.Command {
@@ -53,11 +54,14 @@ func NewFoldCommand(rootname string, groupid string) *cobra.Command {
5354
configfile: "schema.yaml",
5455
notrim: false,
5556
charset: charsets.ISO88591,
57+
usecomma: false,
5658
}
5759

5860
fold.cmd.Flags().StringVarP(&fold.configfile, "schema", "s", fold.configfile, "set the schema file")
5961
fold.cmd.Flags().BoolVarP(&fold.notrim, "notrim", "t", fold.notrim, "don't trim input by default")
6062
fold.cmd.Flags().StringVarP(&fold.charset, "charset", "c", fold.charset, "set the charset for input records")
63+
fold.cmd.Flags().BoolVarP(&fold.usecomma, "usecomma", "u", fold.usecomma,
64+
"use comma as decimal separator instead of dot")
6165

6266
fold.cmd.RunE = func(cmd *cobra.Command, args []string) error {
6367
if err := fold.execute(cmd, args); err != nil {
@@ -84,7 +88,12 @@ func (f *Fold) execute(cmd *cobra.Command, _ []string) error {
8488
return fmt.Errorf("failed to load configuration file : %w", err)
8589
}
8690

87-
schema, err := cfg.Compile(config.Trim(!f.notrim), config.Charset(f.charset))
91+
decimal := config.Comp3Dot()
92+
if f.usecomma {
93+
decimal = config.Comp3Comma()
94+
}
95+
96+
schema, err := cfg.Compile(config.Trim(!f.notrim), config.Charset(f.charset), decimal)
8897
if err != nil {
8998
return fmt.Errorf("failed to compile configuration file : %w", err)
9099
}

internal/appli/command/testdata/fold/13-comp3.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description: >-
55
stdin: testdata/fold/13-comp3/stdin.fixed-width
66
flags:
77
-s: testdata/fold/13-comp3/schema.yaml
8+
-u: true # use comma as decimal separator
89
loglevel: error
910
expected:
1011
stdout: testdata/fold/13-comp3/stdout.jsonl
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
{"compressed":"12345.67"}
2-
{"compressed":"+12345.67"}
3-
{"compressed":"-12345.67"}
1+
{"compressed":"12345,67"}
2+
{"compressed":"+12345,67"}
3+
{"compressed":"-12345,67"}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{"compressed":"12345.67"}
2-
{"compressed":"+12345.67"}
2+
{"compressed":"+12345,67"}
33
{"compressed":"-12345.67"}
4-
{"compressed":"123.45"}
4+
{"compressed":"123,45"}

internal/appli/command/unfold.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type Unfold struct {
3737

3838
configfile string
3939
charset string
40+
usecomma bool
4041
}
4142

4243
func NewUnfoldCommand(rootname string, groupid string) *cobra.Command {
@@ -51,10 +52,13 @@ func NewUnfoldCommand(rootname string, groupid string) *cobra.Command {
5152
},
5253
configfile: "schema.yaml",
5354
charset: charsets.ISO88591,
55+
usecomma: false,
5456
}
5557

5658
unfold.cmd.Flags().StringVarP(&unfold.configfile, "schema", "s", unfold.configfile, "set the schema file")
5759
unfold.cmd.Flags().StringVarP(&unfold.charset, "charset", "c", unfold.charset, "set the charset for output records") //nolint:lll
60+
unfold.cmd.Flags().BoolVarP(&unfold.usecomma, "usecomma", "u", unfold.usecomma,
61+
"use comma as decimal separator instead of dot")
5862

5963
unfold.cmd.RunE = func(cmd *cobra.Command, args []string) error {
6064
if err := unfold.execute(cmd, args); err != nil {
@@ -81,7 +85,12 @@ func (u *Unfold) execute(cmd *cobra.Command, _ []string) error {
8185
return fmt.Errorf("failed to load configuration file : %w", err)
8286
}
8387

84-
schema, err := cfg.Compile(config.Trim(true), config.Charset(u.charset))
88+
decimal := config.Comp3Dot()
89+
if u.usecomma {
90+
decimal = config.Comp3Comma()
91+
}
92+
93+
schema, err := cfg.Compile(config.Trim(true), config.Charset(u.charset), decimal)
8594
if err != nil {
8695
return fmt.Errorf("failed to compile configuration file : %w", err)
8796
}

internal/appli/config/model.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type Field struct {
5555
Charset *string `yaml:"charset,omitempty"` // Charset can be nil if not set in the configuration file
5656
Picture Picture `yaml:"picture,omitempty"` // Picture is an optional string representation of the format
5757
Codec string `yaml:"codec,omitempty"` // Codec is the codec to use for this field, default to String
58+
Decimal rune `yaml:"decimal,omitempty"` // Decimal is the decimal separator to use for this field, default to '.'
5859

5960
Schema Either[string, Schema] `yaml:"schema"` // Schema is either a filename (external schema) or an embedded schema
6061
}
@@ -115,7 +116,7 @@ func (f Field) Compile(record *schema.Record, defaults ...Default) (*schema.Reco
115116
return nil, fmt.Errorf("failed to compile picture for field %s: %w", f.Name, err)
116117
}
117118

118-
record = record.WithField(f.Name, codec.NewComp3(format.Length, format.Decimal, format.Signed),
119+
record = record.WithField(f.Name, codec.NewComp3(format.Length, format.Decimal, format.Signed, f.Decimal),
119120
f.CompileOptions()...)
120121
default:
121122
record = record.WithField(f.Name, codec.NewString(charset, f.Length, *f.Trim), f.CompileOptions()...)
@@ -193,3 +194,19 @@ func Charset(name string) Default {
193194
return field
194195
}
195196
}
197+
198+
func Comp3Dot() Default {
199+
return func(field Field) Field {
200+
field.Decimal = '.'
201+
202+
return field
203+
}
204+
}
205+
206+
func Comp3Comma() Default {
207+
return func(field Field) Field {
208+
field.Decimal = ','
209+
210+
return field
211+
}
212+
}

pkg/posimap/core/codec/comp3.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222
"fmt"
2323
"io"
24+
"slices"
2425
"strings"
2526

2627
"github.com/cgi-fr/posimap/pkg/posimap/api"
@@ -38,11 +39,12 @@ type Comp3 struct {
3839
intDigits int
3940
decDigits int
4041
signed bool
41-
size int // number of bytes needed to store the COMP-3 value
42-
length int // number of characters in the string representation (not counting the sign)
42+
size int // number of bytes needed to store the COMP-3 value
43+
length int // number of characters in the string representation (not counting the sign)
44+
sep rune // decimal separator to use in the string representation while decoding comp-3
4345
}
4446

45-
func NewComp3(intDigits, decDigits int, signed bool) *Comp3 {
47+
func NewComp3(intDigits, decDigits int, signed bool, sep rune) *Comp3 {
4648
length := intDigits + decDigits
4749
if decDigits > 0 {
4850
length++
@@ -54,6 +56,7 @@ func NewComp3(intDigits, decDigits int, signed bool) *Comp3 {
5456
signed: signed,
5557
size: (intDigits + decDigits + 2) / 2, //nolint:mnd
5658
length: length,
59+
sep: sep,
5760
}
5861
}
5962

@@ -77,7 +80,7 @@ func (c *Comp3) Decode(buffer api.Buffer, offset int) (any, error) {
7780

7881
for byteIndex, byteVal := range bytes {
7982
if byteIndex*2 == c.intDigits {
80-
result.WriteRune('.')
83+
result.WriteRune(c.sep)
8184
}
8285

8386
if byteIndex == c.size-1 {
@@ -98,7 +101,7 @@ func (c *Comp3) Decode(buffer api.Buffer, offset int) (any, error) {
98101
result.WriteRune(convertNibbleToRune((byteVal & highNibbleMask) >> nibbleShift))
99102

100103
if byteIndex*2+1 == c.intDigits {
101-
result.WriteRune('.')
104+
result.WriteRune(c.sep)
102105
}
103106

104107
result.WriteRune(convertNibbleToRune(byteVal & lowNibbleMask))
@@ -113,13 +116,14 @@ func (c *Comp3) Encode(buffer api.Buffer, offset int, value any) error {
113116
return err
114117
}
115118

116-
if c.decDigits > 0 && str[c.intDigits] != '.' {
119+
// accept both '.' and ',' as valid decimal separator while encoding ensure robustness
120+
if c.decDigits > 0 && str[c.intDigits] != '.' && str[c.intDigits] != ',' {
117121
return fmt.Errorf("%w: expected decimal separator at position %d, got %q",
118122
ErrMisplacedDecimalSep, c.intDigits, str[c.intDigits])
119123
}
120124

121125
// ensure that there is only one decimal separator
122-
if strings.Count(str, ".") > 1 {
126+
if strings.Count(str, ".")+strings.Count(str, ",") > 1 {
123127
return fmt.Errorf("%w: too many decimal separators in COMP-3 encoding", ErrMisplacedDecimalSep)
124128
}
125129

@@ -171,7 +175,7 @@ func (c *Comp3) encode(buffer api.Buffer, offset int, str string, nibbleSign byt
171175
return fmt.Errorf("%w: too many characters in COMP-3 encoding", ErrBufferTooShort)
172176
}
173177

174-
if char == '.' {
178+
if slices.Contains([]rune{'.', ','}, char) {
175179
if charIndex-ignoreCount != c.intDigits {
176180
return fmt.Errorf("%w", ErrMisplacedDecimalSep)
177181
}

0 commit comments

Comments
 (0)