Skip to content

Commit 6b6e276

Browse files
committed
#776 Add BCD encoders for use by the writer.
1 parent 7f63354 commit 6b6e276

File tree

2 files changed

+309
-0
lines changed

2 files changed

+309
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
* Copyright 2018 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package za.co.absa.cobrix.cobol.parser.encoding
18+
19+
import java.math.RoundingMode
20+
21+
object BCDNumberEncoders {
22+
/**
23+
* Encode a number as a binary encoded decimal (BCD) aka COMP-3 format to an array of bytes.
24+
*
25+
* The length of the output array is determined by the formula: (precision + 1) / 2.
26+
*
27+
* @param number The number to encode.
28+
* @param precision Total number of digits in the number.
29+
* @param scale A decimal scale if a number is a decimal. Should be greater or equal to zero.
30+
* @param scaleFactor Additional zeros to be added before of after the decimal point.
31+
* @param signed if true, sign nibble is added and negative numbers are supported.
32+
* @param mandatorySignNibble If true, the BCD number should contain the sign nibble. Otherwise, the number is
33+
* considered unsigned, and negative numbers are encoded as null (zero bytes).
34+
* @return A BCD representation of the number, array of zero bytes if the data is not properly formatted.
35+
*/
36+
def encodeBCDNumber(number: java.math.BigDecimal,
37+
precision: Int,
38+
scale: Int,
39+
scaleFactor: Int,
40+
signed: Boolean,
41+
mandatorySignNibble: Boolean): Array[Byte] = {
42+
if (precision < 1)
43+
throw new IllegalArgumentException(s"Invalid BCD precision=$precision, should be greater than zero.")
44+
45+
val totalDigits = if (mandatorySignNibble) {
46+
if (precision % 2 == 0) precision + 2 else precision + 1
47+
} else {
48+
if (precision % 2 == 0) precision else precision + 1
49+
}
50+
51+
val byteCount = totalDigits / 2
52+
val bytes = new Array[Byte](byteCount)
53+
54+
if (number == null) {
55+
return bytes
56+
}
57+
58+
val integralNumberStr = if (scaleFactor - scale == 0)
59+
number.setScale(0, RoundingMode.HALF_DOWN).toString
60+
else
61+
number.movePointLeft(scaleFactor - scale).setScale(0, RoundingMode.HALF_DOWN).toString
62+
63+
val isNegative = integralNumberStr.startsWith("-")
64+
val digitsOnly = integralNumberStr.stripPrefix("-").stripPrefix("+")
65+
66+
if (isNegative && (!signed || !mandatorySignNibble)) {
67+
return bytes
68+
}
69+
70+
val signNibble: Byte = if (signed) {
71+
if (isNegative) 0x0D else 0x0C
72+
} else {
73+
0x0F
74+
}
75+
76+
if (digitsOnly.length > precision)
77+
return bytes
78+
79+
val padded = if (mandatorySignNibble) {
80+
if (digitsOnly.length == totalDigits - 1)
81+
digitsOnly + "0"
82+
else
83+
"0"*(totalDigits - digitsOnly.length - 1) + digitsOnly + "0"
84+
} else {
85+
if (digitsOnly.length == totalDigits)
86+
digitsOnly
87+
else
88+
"0"*(totalDigits - digitsOnly.length) + digitsOnly
89+
}
90+
91+
var bi = 0
92+
93+
while (bi < byteCount) {
94+
val high = padded.charAt(bi * 2).asDigit
95+
val low = padded.charAt(bi * 2 + 1).asDigit
96+
97+
bytes(bi) = ((high << 4) | low).toByte
98+
bi += 1
99+
}
100+
101+
if (mandatorySignNibble) {
102+
bytes(byteCount - 1) = ((bytes(byteCount - 1) & 0xF0) | signNibble).toByte
103+
}
104+
105+
bytes
106+
}
107+
108+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright 2018 ABSA Group Limited
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package za.co.absa.cobrix.cobol.parser.encoding
18+
19+
import org.scalatest.Assertion
20+
import org.scalatest.wordspec.AnyWordSpec
21+
22+
class BCDNumberEncodersSuite extends AnyWordSpec {
23+
"encodeBCDNumber" should {
24+
"integral number" when {
25+
"encode a number" in {
26+
val expected = Array[Byte](0x12, 0x34, 0x5C)
27+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(12345), 5, 0, 0, signed = true, mandatorySignNibble = true)
28+
29+
checkExpected(actual, expected)
30+
}
31+
32+
"encode a small number" in {
33+
val expected = Array[Byte](0x00, 0x00, 0x5C)
34+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(5), 5, 0, 0, signed = true, mandatorySignNibble = true)
35+
36+
checkExpected(actual, expected)
37+
}
38+
39+
"encode an unsigned number" in {
40+
val expected = Array[Byte](0x12, 0x34, 0x5F)
41+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(12345), 5, 0, 0, signed = false, mandatorySignNibble = true)
42+
43+
checkExpected(actual, expected)
44+
}
45+
46+
"encode a negative number" in {
47+
val expected = Array[Byte](0x12, 0x34, 0x5D)
48+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(-12345), 5, 0, 0, signed = true, mandatorySignNibble = true)
49+
50+
checkExpected(actual, expected)
51+
}
52+
53+
"encode a small negative number" in {
54+
val expected = Array[Byte](0x00, 0x00, 0x7D)
55+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(-7), 4, 0, 0, signed = true, mandatorySignNibble = true)
56+
57+
checkExpected(actual, expected)
58+
}
59+
60+
"encode a number without sign nibble" in {
61+
val expected = Array[Byte](0x01, 0x23, 0x45)
62+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(12345), 5, 0, 0, signed = false, mandatorySignNibble = false)
63+
64+
checkExpected(actual, expected)
65+
}
66+
67+
"encode a too big number" in {
68+
val expected = Array[Byte](0x00, 0x00, 0x00)
69+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(123456), 5, 0, 0, signed = false, mandatorySignNibble = false)
70+
71+
checkExpected(actual, expected)
72+
}
73+
74+
"encode a too big negative number" in {
75+
val expected = Array[Byte](0x00, 0x00, 0x00)
76+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(-123456), 5, 0, 0, signed = true, mandatorySignNibble = true)
77+
78+
checkExpected(actual, expected)
79+
}
80+
81+
"attempt to encode a negative number without sign nibble" in {
82+
val expected = Array[Byte](0x00, 0x00, 0x00)
83+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(-12345), 5, 0, 0, signed = false, mandatorySignNibble = false)
84+
85+
checkExpected(actual, expected)
86+
}
87+
88+
"attempt to encode a signed number without a sign nibble" in {
89+
val expected = Array[Byte](0x00, 0x00, 0x00)
90+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(-12345), 5, 0, 0, signed = true, mandatorySignNibble = false)
91+
92+
checkExpected(actual, expected)
93+
}
94+
95+
"attempt to encode a number with an incorrect precision" in {
96+
val expected = Array[Byte](0x00, 0x00)
97+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(12345), 4, 0, 0, signed = false, mandatorySignNibble = false)
98+
99+
checkExpected(actual, expected)
100+
}
101+
102+
"attempt to encode a number with an incorrect precision with sign nibble" in {
103+
val expected = Array[Byte](0x00, 0x00, 0x00)
104+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(12345), 4, 0, 0, signed = true, mandatorySignNibble = true)
105+
106+
checkExpected(actual, expected)
107+
}
108+
}
109+
110+
"decimal number" when {
111+
"encode a number" in {
112+
val expected = Array[Byte](0x12, 0x34, 0x5C)
113+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(123.45), 5, 2, 0, signed = true, mandatorySignNibble = true)
114+
115+
checkExpected(actual, expected)
116+
}
117+
118+
"encode a small number" in {
119+
val expected = Array[Byte](0x00, 0x00, 0x5C)
120+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(0.05), 5, 2, 0, signed = true, mandatorySignNibble = true)
121+
122+
checkExpected(actual, expected)
123+
}
124+
125+
"encode an unsigned number" in {
126+
val expected = Array[Byte](0x12, 0x34, 0x5F)
127+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(1234.5), 5, 1, 0, signed = false, mandatorySignNibble = true)
128+
129+
checkExpected(actual, expected)
130+
}
131+
132+
"encode a negative number" in {
133+
val expected = Array[Byte](0x12, 0x34, 0x5D)
134+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(-12.345), 5, 3, 0, signed = true, mandatorySignNibble = true)
135+
136+
checkExpected(actual, expected)
137+
}
138+
139+
"encode a small negative number" in {
140+
val expected = Array[Byte](0x00, 0x00, 0x7D)
141+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(-0.00007), 4, 5, 0, signed = true, mandatorySignNibble = true)
142+
143+
checkExpected(actual, expected)
144+
}
145+
146+
"encode a number without sign nibble" in {
147+
val expected = Array[Byte](0x01, 0x23, 0x45)
148+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(123.45), 5, 2, 0, signed = false, mandatorySignNibble = false)
149+
150+
checkExpected(actual, expected)
151+
}
152+
153+
"encode a too precise number" in {
154+
val expected = Array[Byte](0x01, 0x23, 0x46)
155+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(123.456), 5, 2, 0, signed = false, mandatorySignNibble = false)
156+
157+
checkExpected(actual, expected)
158+
}
159+
160+
"encode a too big number" in {
161+
val expected = Array[Byte](0x00, 0x00, 0x00)
162+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(1234.56), 5, 2, 0, signed = false, mandatorySignNibble = false)
163+
164+
checkExpected(actual, expected)
165+
}
166+
167+
"encode a too big negative number" in {
168+
val expected = Array[Byte](0x00, 0x00, 0x00)
169+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(-1234.56), 5, 2, 0, signed = true, mandatorySignNibble = true)
170+
171+
checkExpected(actual, expected)
172+
}
173+
174+
"encode a number with positive scale factor" in {
175+
val expected = Array[Byte](0x00, 0x12, 0x3F)
176+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(12300), 5, 0, 2, signed = false, mandatorySignNibble = true)
177+
178+
checkExpected(actual, expected)
179+
}
180+
181+
"encode a number with negative scale factor" in {
182+
val expected = Array[Byte](0x00, 0x12, 0x3F)
183+
val actual = BCDNumberEncoders.encodeBCDNumber(new java.math.BigDecimal(1.23), 5, 0, -2, signed = false, mandatorySignNibble = true)
184+
185+
checkExpected(actual, expected)
186+
}
187+
}
188+
}
189+
190+
def checkExpected(actual: Array[Byte], expected: Array[Byte]): Assertion = {
191+
if (!actual.sameElements(expected)) {
192+
val actualHex = actual.map(b => f"$b%02X").mkString(" ")
193+
val expectedHex = expected.map(b => f"$b%02X").mkString(" ")
194+
fail(s"Actual: $actualHex\nExpected: $expectedHex")
195+
} else {
196+
succeed
197+
}
198+
}
199+
200+
201+
}

0 commit comments

Comments
 (0)