Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import scala.collection.mutable.ArrayBuffer


class Copybook(val ast: CopybookAST) extends Logging with Serializable {
import Copybook._

def getCobolSchema: CopybookAST = ast

Expand Down Expand Up @@ -215,38 +216,6 @@ class Copybook(val ast: CopybookAST) extends Logging with Serializable {
field.decodeTypeValue(0, slicedBytes)
}

/**
* Set value of a field of the copybook record by the AST object of the field
*
* Nested field names can contain '.' to identify the exact field.
* If the field name is unique '.' is not required.
*
* @param field The AST object of the field
* @param bytes Binary encoded data of the record
* @param startOffset An offset to the beginning of the field in the data (in bytes).
* @return The value of the field
*
*/
def setPrimitiveField(field: Primitive, recordBytes: Array[Byte], value: Any, startOffset: Int = 0): Unit = {
field.encode match {
case Some(encode) =>
val fieldBytes = encode(value)
val startByte = field.binaryProperties.offset + startOffset
val endByte = field.binaryProperties.offset + startOffset + field.binaryProperties.actualSize

if (startByte < 0 || endByte > recordBytes.length) {
throw new IllegalArgumentException(s"Cannot set value for field '${field.name}' because the field is out of bounds of the record.")
}
if (fieldBytes.length != field.binaryProperties.dataSize) {
throw new IllegalArgumentException(s"Cannot set value for field '${field.name}' because the encoded value has a different size than the field size.")
}

System.arraycopy(fieldBytes, 0, recordBytes, startByte, fieldBytes.length)
case None =>
throw new IllegalStateException(s"Cannot set value for field '${field.name}' because it does not have an encoder defined.")
}
}

/** This routine is used for testing by generating a layout position information to compare with mainframe output */
def generateRecordLayoutPositions(): String = {
var fieldCounter: Int = 0
Expand Down Expand Up @@ -442,4 +411,36 @@ object Copybook {

new Copybook(schema)
}

/**
* Set value of a field of the copybook record by the AST object of the field
*
* Nested field names can contain '.' to identify the exact field.
* If the field name is unique '.' is not required.
*
* @param field The AST object of the field
* @param recordBytes Binary encoded data of the record
* @param startOffset An offset to the beginning of the field in the data (in bytes).
* @return The value of the field
*
*/
def setPrimitiveField(field: Primitive, recordBytes: Array[Byte], value: Any, startOffset: Int = 0): Unit = {
field.encode match {
case Some(encode) =>
val fieldBytes = encode(value)
val startByte = field.binaryProperties.offset + startOffset
val endByte = field.binaryProperties.offset + startOffset + field.binaryProperties.actualSize

if (startByte < 0 || endByte > recordBytes.length) {
throw new IllegalArgumentException(s"Cannot set value for field '${field.name}' because the field is out of bounds of the record.")
}
if (fieldBytes.length != field.binaryProperties.dataSize) {
throw new IllegalArgumentException(s"Cannot set value for field '${field.name}' because the encoded value has a different size than the field size.")
}

System.arraycopy(fieldBytes, 0, recordBytes, startByte, fieldBytes.length)
case None =>
throw new IllegalStateException(s"Cannot set value for field '${field.name}' because it does not have an encoder defined.")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright 2018 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.cobrix.cobol.parser.encoding

import java.math.RoundingMode

object BCDNumberEncoders {
/**
* Encode a number as a binary encoded decimal (BCD) aka COMP-3 format to an array of bytes.
*
* Output length (bytes):
* - With mandatory sign nibble (signed or unsigned): ceil((precision + 1) / 2)
* - Unsigned without sign nibble: ceil(precision / 2).
*
* @param number The number to encode.
* @param precision Total number of digits in the number.
* @param scale A decimal scale if a number is a decimal. Should be greater or equal to zero.
* @param scaleFactor Additional zeros to be added before of after the decimal point.
* @param signed if true, sign nibble is added and negative numbers are supported.
* @param mandatorySignNibble If true, the BCD number should contain the sign nibble. Otherwise, the number is
* considered unsigned, and negative numbers are encoded as null (zero bytes).
* @return A BCD representation of the number, array of zero bytes if the data is not properly formatted.
*/
def encodeBCDNumber(number: java.math.BigDecimal,
precision: Int,
scale: Int,
scaleFactor: Int,
signed: Boolean,
mandatorySignNibble: Boolean): Array[Byte] = {
if (precision < 1)
throw new IllegalArgumentException(s"Invalid BCD precision=$precision, should be greater than zero.")

val totalDigits = if (mandatorySignNibble) {
if (precision % 2 == 0) precision + 2 else precision + 1
} else {
if (precision % 2 == 0) precision else precision + 1
}

val byteCount = totalDigits / 2
val bytes = new Array[Byte](byteCount)

if (number == null) {
return bytes
}

val shift = scaleFactor - scale
val shifted = if (shift == 0) number else number.movePointLeft(shift)

val isNegative = number.signum() < 0
val digitsOnly = shifted.abs().setScale(0, RoundingMode.HALF_DOWN).toPlainString

if (isNegative && (!signed || !mandatorySignNibble)) {
return bytes
}

if (digitsOnly.length > precision || scale < 0)
return bytes

val signNibble: Byte = if (signed) {
if (isNegative) 0x0D else 0x0C
} else {
0x0F
}

val padded = if (mandatorySignNibble) {
if (digitsOnly.length == totalDigits - 1)
digitsOnly + "0"
else
"0"*(totalDigits - digitsOnly.length - 1) + digitsOnly + "0"
} else {
if (digitsOnly.length == totalDigits)
digitsOnly
else
"0"*(totalDigits - digitsOnly.length) + digitsOnly
}

var bi = 0

while (bi < byteCount) {
val high = padded.charAt(bi * 2).asDigit
val low = padded.charAt(bi * 2 + 1).asDigit

bytes(bi) = ((high << 4) | low).toByte
bi += 1
}

if (mandatorySignNibble) {
bytes(byteCount - 1) = ((bytes(byteCount - 1) & 0xF0) | signNibble).toByte
}

bytes
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package za.co.absa.cobrix.cobol.parser.encoding

import za.co.absa.cobrix.cobol.parser.ast.datatype.{AlphaNumeric, CobolType}
import za.co.absa.cobrix.cobol.parser.ast.datatype.{AlphaNumeric, COMP3, COMP3U, CobolType, Decimal, Integral}
import za.co.absa.cobrix.cobol.parser.encoding.codepage.{CodePage, CodePageCommon}

import java.nio.charset.{Charset, StandardCharsets}
Expand All @@ -31,6 +31,14 @@ object EncoderSelector {
dataType match {
case alphaNumeric: AlphaNumeric if alphaNumeric.compact.isEmpty =>
getStringEncoder(alphaNumeric.enc.getOrElse(EBCDIC), ebcdicCodePage, asciiCharset, alphaNumeric.length)
case integralComp3: Integral if integralComp3.compact.exists(_.isInstanceOf[COMP3]) =>
Option(getBdcEncoder(integralComp3.precision, 0, 0, integralComp3.signPosition.isDefined, mandatorySignNibble = true))
case integralComp3: Integral if integralComp3.compact.exists(_.isInstanceOf[COMP3U]) =>
Option(getBdcEncoder(integralComp3.precision, 0, 0, integralComp3.signPosition.isDefined, mandatorySignNibble = false))
case decimalComp3: Decimal if decimalComp3.compact.exists(_.isInstanceOf[COMP3]) =>
Option(getBdcEncoder(decimalComp3.precision, decimalComp3.scale, decimalComp3.scaleFactor, decimalComp3.signPosition.isDefined, mandatorySignNibble = true))
case decimalComp3: Decimal if decimalComp3.compact.exists(_.isInstanceOf[COMP3U]) =>
Option(getBdcEncoder(decimalComp3.precision, decimalComp3.scale, decimalComp3.scaleFactor, decimalComp3.signPosition.isDefined, mandatorySignNibble = false))
case _ =>
None
}
Expand Down Expand Up @@ -80,4 +88,26 @@ object EncoderSelector {
buf
}

def getBdcEncoder(precision: Int,
scale: Int,
scaleFactor: Int,
signed: Boolean,
mandatorySignNibble: Boolean): Encoder = {
if (signed && !mandatorySignNibble)
throw new IllegalArgumentException("If signed is true, mandatorySignNibble must also be true.")

(a: Any) => {
val number = a match {
case null => null
case d: java.math.BigDecimal => d
case n: java.math.BigInteger => new java.math.BigDecimal(n)
case n: Byte => new java.math.BigDecimal(n)
case n: Int => new java.math.BigDecimal(n)
case n: Long => new java.math.BigDecimal(n)
case x => new java.math.BigDecimal(x.toString)
}
BCDNumberEncoders.encodeBCDNumber(number, precision, scale, scaleFactor, signed, mandatorySignNibble)
}
}

}
Loading
Loading