C #: Convierta el decimal empaquetado COMP-3 a valor legible por humanos

Tengo una serie de archivos planos ASCII que vienen de un mainframe para ser procesados ​​por una aplicación C #. Se ha introducido una nueva fuente con un campo de decimal empaquetado (COMP-3), que debe convertirse en un valor numérico.

Los archivos se transfieren a través de FTP, utilizando el modo de transferencia ASCII. Me preocupa que el campo binario pueda contener lo que se interpretará como códigos ASCII muy bajos o caracteres de control en lugar de un valor. O peor aún, puede perderse en el proceso de FTP.

Además, los campos se leen como cadenas. Puede que tenga la flexibilidad para trabajar alrededor de esta parte (es decir, una secuencia de algún tipo), pero el negocio me dará un rechazo.

El requisito decía “Convertir de HEX a ASCII”, pero claramente eso no daba los valores correctos. Cualquier ayuda sería apreciada; no es necesario que sea específico del idioma, siempre que pueda explicar la lógica del proceso de conversión.

En primer lugar, debe eliminar los problemas de traducción de final de línea (EOL) que serán causados ​​por el modo de transferencia ASCII. Usted tiene toda la razón de preocuparse por la corrupción de datos cuando los valores BCD corresponden a los caracteres EOL. El peor aspecto de este problema es que ocurrirá de manera rara e inesperada.

La mejor solución es cambiar el modo de transferencia a BIN. Esto es apropiado ya que los datos que está transfiriendo son binarios. Si no es posible utilizar el modo de transferencia FTP correcto, puede deshacer el daño del modo ASCII en el código. Todo lo que tienes que hacer es convertir \ r \ n pares de nuevo a \ n. Si fuera tú, me aseguraría de que esto esté bien probado.

Una vez que haya resuelto el problema EOL, la conversión COMP-3 es bastante directa. Pude encontrar este artículo en la base de conocimientos de MS con código de ejemplo en BASIC. Vea a continuación un puerto VB.NET de este código.

Ya que está tratando con valores COMP-3, el formato de archivo que está leyendo seguramente tiene tamaños de registro fijos con longitudes de campo fijas. Si fuera usted, obtendría una especificación de formato de archivo antes de continuar con esto. Debería estar utilizando un BinaryReader para trabajar con estos datos. Si alguien está rechazando este punto, me iría. Permítales encontrar a alguien más para complacer su locura.

Aquí hay un puerto VB.NET del código de ejemplo BASIC. No he probado esto porque no tengo acceso a un archivo COMP-3. Si esto no funciona, me referiría al código de ejemplo original de MS para obtener orientación, o a las referencias en las otras respuestas a esta pregunta.

Imports Microsoft.VisualBasic Module Module1 'Sample COMP-3 conversion code 'Adapted from http://support.microsoft.com/kb/65323 'This code has not been tested Sub Main() Dim Digits%(15) 'Holds the digits for each number (max = 16). Dim Basiceqv#(1000) 'Holds the Basic equivalent of each COMP-3 number. 'Added to make code compile Dim MyByte As Char, HighPower%, HighNibble% Dim LowNibble%, Digit%, E%, Decimal%, FileName$ 'Clear the screen, get the filename and the amount of decimal places 'desired for each number, and open the file for sequential input: FileName$ = InputBox("Enter the COBOL data file name: ") Decimal% = InputBox("Enter the number of decimal places desired: ") FileOpen(1, FileName$, OpenMode.Binary) Do Until EOF(1) 'Loop until the end of the file is reached. Input(1, MyByte) If MyByte = Chr(0) Then 'Check if byte is 0 (ASC won't work on 0). Digits%(HighPower%) = 0 'Make next two digits 0. Increment Digits%(HighPower% + 1) = 0 'the high power to reflect the HighPower% = HighPower% + 2 'number of digits in the number 'plus 1. Else HighNibble% = Asc(MyByte) \ 16 'Extract the high and low LowNibble% = Asc(MyByte) And &HF 'nibbles from the byte. The Digits%(HighPower%) = HighNibble% 'high nibble will always be a 'digit. If LowNibble% <= 9 Then 'If low nibble is a 'digit, assign it and Digits%(HighPower% + 1) = LowNibble% 'increment the high HighPower% = HighPower% + 2 'power accordingly. Else HighPower% = HighPower% + 1 'Low nibble was not a digit but a Digit% = 0 '+ or - signals end of number. 'Start at the highest power of 10 for the number and multiply 'each digit by the power of 10 place it occupies. For Power% = (HighPower% - 1) To 0 Step -1 Basiceqv#(E%) = Basiceqv#(E%) + (Digits%(Digit%) * (10 ^ Power%)) Digit% = Digit% + 1 Next 'If the sign read was negative, make the number negative. If LowNibble% = 13 Then Basiceqv#(E%) = Basiceqv#(E%) - (2 * Basiceqv#(E%)) End If 'Give the number the desired amount of decimal places, print 'the number, increment E% to point to the next number to be 'converted, and reinitialize the highest power. Basiceqv#(E%) = Basiceqv#(E%) / (10 ^ Decimal%) Print(Basiceqv#(E%)) E% = E% + 1 HighPower% = 0 End If End If Loop FileClose() 'Close the COBOL data file, and end. End Sub End Module 

He estado viendo las publicaciones en numerosos tableros relacionados con la conversión de datos BCD de Comp-3 de archivos de mainframe “heredados” a algo utilizable en C #. Primero, me gustaría decir que estoy menos que enamorado de las respuestas que han recibido algunas de estas publicaciones, especialmente las que han dicho esencialmente “por qué nos molesta con estas publicaciones no relacionadas con C # / C ++” y también “Si Necesita una respuesta sobre algún tipo de convención COBOL, ¿por qué no visita un sitio orientado a COBOL? Para mí, esto es un BS completo, ya que probablemente se necesitarán muchos años (desafortunadamente) para que los desarrolladores de software entiendan cómo lidiar con algunos de estos problemas heredados que existen en EL MUNDO REAL. Entonces, incluso si me cierran de golpe en esta publicación por el siguiente código, voy a compartir con ustedes una experiencia de REAL WORLD con la que tuve que lidiar con respecto a la conversión de COMP-3 / EBCDIC (y sí, soy el que habla de ” disquetes, cintas de papel, paquetes de discos, etc … – He sido ingeniero de software desde 1979 “).

Primero: comprenda que cualquier archivo que lea de un sistema de marco principal heredado como IBM le presentará los datos en formato EBCDIC y para convertir cualquiera de esos datos a una cadena C # / C ++ con la que pueda lidiar. Tendremos que usar la traducción de la página de códigos adecuada para obtener los datos en formato ASCII. Un buen ejemplo de cómo manejar esto sería:

StreamReader readFile = new StreamReader (ruta, Encoding.GetEncoding (037); // 037 = EBCDIC a ASCII traducción.

Esto asegurará que todo lo que lea de este flujo se convierta a ASCII y se pueda usar en un formato de cadena. Esto incluye los campos “Decimales de zona” (Imagen 9) y “Texto” (Imagen X) tal como lo declaró COBOL. Sin embargo, esto no necesariamente convierte los campos COMP-3 al equivelante “binario” correcto cuando se lee en una matriz char [] o byte []. Para hacer esto, la única forma en que alguna vez va a traducir esto correctamente (incluso si usa las páginas de códigos UTF-8, UTF-16, Predeterminado o lo que sea), querrá abrir el archivo de esta manera:

FileStream fileStream = nuevo FileStream (ruta, FIleMode.Open, FIleAccess.Read, FileShare.Read);

Por supuesto, la opción “FileShare.Read” es “opcional”.

Cuando haya aislado el campo que desea convertir a un valor decimal (y luego a una cadena ASCII si es necesario), puede usar el siguiente código, y esto ha sido básicamente robado de la publicación de MicroSoft “UnpackDecimal” que puede llegar a

http://www.microsoft.com/downloads/details.aspx?familyid=0e4bba52-cc52-4d89-8590-cda297ff7fbd&displaylang=en

He aislado (creo) cuáles son las partes más importantes de esta lógica y las he consolidado en dos métodos que puede hacer con lo que quiere. Para mis propósitos, elegí dejar esto como devolver un valor decimal que luego podría hacer con lo que quería. Básicamente, el método se llama “desempaquetar” y se le pasa una matriz de bytes [] (no más de 12 bytes) y la escala como un int, que es el número de lugares decimales que desea devolver en el valor decimal. Espero que esto funcione para usted tan bien como lo hizo para mí.

  private Decimal Unpack(byte[] inp, int scale) { long lo = 0; long mid = 0; long hi = 0; bool isNegative; // this nybble stores only the sign, not a digit. // "C" hex is positive, "D" hex is negative, and "F" hex is unsigned. switch (nibble(inp, 0)) { case 0x0D: isNegative = true; break; case 0x0F: case 0x0C: isNegative = false; break; default: throw new Exception("Bad sign nibble"); } long intermediate; long carry; long digit; for (int j = inp.Length * 2 - 1; j > 0; j--) { // multiply by 10 intermediate = lo * 10; lo = intermediate & 0xffffffff; carry = intermediate >> 32; intermediate = mid * 10 + carry; mid = intermediate & 0xffffffff; carry = intermediate >> 32; intermediate = hi * 10 + carry; hi = intermediate & 0xffffffff; carry = intermediate >> 32; // By limiting input length to 14, we ensure overflow will never occur digit = nibble(inp, j); if (digit > 9) { throw new Exception("Bad digit"); } intermediate = lo + digit; lo = intermediate & 0xffffffff; carry = intermediate >> 32; if (carry > 0) { intermediate = mid + carry; mid = intermediate & 0xffffffff; carry = intermediate >> 32; if (carry > 0) { intermediate = hi + carry; hi = intermediate & 0xffffffff; carry = intermediate >> 32; // carry should never be non-zero. Back up with validation } } } return new Decimal((int)lo, (int)mid, (int)hi, isNegative, (byte)scale); } private int nibble(byte[] inp, int nibbleNo) { int b = inp[inp.Length - 1 - nibbleNo / 2]; return (nibbleNo % 2 == 0) ? (b & 0x0000000F) : (b >> 4); } 

Si tiene alguna pregunta, publíquela aquí, porque sospecho que me voy a “incendiar” como todos los que eligieron publicar preguntas que sean pertinentes a los problemas de hoy …

Gracias, John – The Elder.

Si los datos originales estaban en EBCDIC, su campo COMP-3 ha sido confuso. El proceso de FTP ha realizado una conversión de EBCDIC a ASCII de los valores de byte en el campo COMP-3 que no es lo que desea. Para corregir esto puedes:

1) Use el modo BINARIO para la transferencia para obtener los datos EBCDIC sin procesar. Luego, convierte el campo COMP-3 en un número y convierte cualquier otro texto EBCDIC en el registro a ASCII. Un campo empaquetado almacena cada dígito en medio byte con el medio byte inferior como signo (F es positivo y otros valores, generalmente D o E, son negativos). El almacenamiento de 123.4 en un PIC 999.99 USAGE COMP-3 sería X’01234F ‘(tres bytes) y -123 en el mismo campo es X’01230D’.

2) Haga que el remitente convierta el campo en un campo numérico. Esto almacena el número como una cadena de dígitos numéricos EBCDIC con el signo como un carácter negativo (-) o en blanco separado. Todos los dígitos y el signo se traducen correctamente a su equivalente ASCII en la transferencia de FTP.

Pido disculpas si estoy fuera de lugar aquí, pero quizás este ejemplo de código que pegaré aquí podría ayudarlo. Esto vino de VBRocks …

 Imports System Imports System.IO Imports System.Text Imports System.Text.Encoding '4/20/07 submission includes a line spacing addition when a control character is used: ' The line spacing is calculated off of the 3rd control character. ' ' Also includes the 4/18 modification of determining end of file. '4/26/07 submission inclues an addition of 6 to the record length when the 4th control ' character is an 8. This is because these records were being truncated. 'Authored by Gary A. Lima, aka. VBRocks '''  ''' Translates an EBCDIC file to an ASCII file. '''  '''  Public Class EBCDIC_to_ASCII_Translator #Region " Example" Private Sub Example() 'Set your source file and destination file paths Dim sSourcePath As String = "c:\Temp\MyEBCDICFile" Dim sDestinationPath As String = "c:\Temp\TranslatedFile.txt" Dim trans As New EBCDIC_to_ASCII_Translator() 'If your EBCDIC file uses Control records to determine the length of a record, then this to True trans.UseControlRecord = True 'If the first record of your EBCDIC file is filler (junk), then set this to True trans.IgnoreFirstRecord = True 'EBCDIC files are written in block lengths, set your block length (Example: 134, 900, Etc.) trans.BlockLength = 900 'This method will actually translate your source file and output it to the specified destination file path trans.TranslateFile(sSourcePath, sDestinationPath) 'Here is a alternate example: 'No Control record is used 'trans.UseControlRecord = False 'Translate the whole file, including the first record 'trans.IgnoreFirstRecord = False 'Set the block length 'trans.BlockLength = 134 'Translate... 'trans.TranslateFile(sSourcePath, sDestinationPath) '*** Some additional methods that you can use are: 'Trim off leading characters from left side of string (position 0 to...) 'trans.LTrim = 15 'Translate 1 EBCDIC character to an ASCII character 'Dim strASCIIChar as String = trans.TranslateCharacter("S") 'Translate an EBCDIC character array to an ASCII string 'trans.TranslateCharacters(chrEBCDICArray) 'Translates an EBCDIC string to an ASCII string 'Dim strASCII As String = trans.TranslateString("EBCDIC String") End Sub #End Region 'Example 'Translate characters from EBCDIC to ASCII Private ASCIIEncoding As Encoding = Encoding.ASCII Private EBCDICEncoding As Encoding = Encoding.GetEncoding(37) 'EBCDIC 'Block Length: Can be fixed (Ex: 134). Private miBlockLength As Integer = 0 Private mbUseControlRec As Boolean = True 'If set to False, will return exact block length Private mbIgnoreFirstRecord As Boolean = True 'Will Ignore first record if set to true (First record may be filler) Private miLTrim As Integer = 0 '''  ''' Translates SourceFile from EBCDIC to ASCII. Writes output to file path specified by DestinationFile parameter. ''' Set the BlockLength Property to designate block size to read. '''  ''' Enter the path of the Source File. ''' Enter the path of the Destination File. '''  Public Sub TranslateFile(ByVal SourceFile As String, ByVal DestinationFile As String) Dim iRecordLength As Integer 'Stores length of a record, not including the length of the Control Record (if used) Dim sRecord As String = "" 'Stores the actual record Dim iLineSpace As Integer = 1 'LineSpace: 1 for Single Space, 2 for Double Space, 3 for Triple Space... Dim iControlPosSix As Byte() 'Stores the 6th character of a Control Record (used to calculate record length) Dim iControlRec As Byte() 'Stores the EBCDIC Control Record (First 6 characters of record) Dim bEOR As Boolean 'End of Record Flag Dim bBOF As Boolean = True 'Beginning of file Dim iConsumedChars As Integer = 0 'Stores the number of consumed characters in the current block Dim bIgnoreRecord As Boolean = mbIgnoreFirstRecord 'Ignores the first record if set. Dim ControlArray(5) As Char 'Stores Control Record (first 6 bytes) Dim chrArray As Char() 'Stores characters just after read from file Dim sr As New StreamReader(SourceFile, EBCDICEncoding) Dim sw As New StreamWriter(DestinationFile) 'Set the RecordLength to the RecordLength Property (below) iRecordLength = miBlockLength 'Loop through entire file Do Until sr.EndOfStream = True 'If using a Control Record, then check record for valid data. If mbUseControlRec = True Then 'Read the Control Record (first 6 characters of the record) sr.ReadBlock(ControlArray, 0, 6) 'Update the value of consumed (read) characters iConsumedChars += ControlArray.Length 'Get the bytes of the Control Record Array iControlRec = EBCDICEncoding.GetBytes(ControlArray) 'Set the line spacing (position 3 divided by 64) ' (64 decimal = Single Spacing; 128 decimal = Double Spacing) iLineSpace = iControlRec(2) / 64 'Check the Control record for End of File 'If the Control record has a 8 or 10 in position 1, and a 1 in postion 2, then it is the end of the file If (iControlRec(0) = 8 OrElse iControlRec(0) = 10) AndAlso _ iControlRec(1) = 1 Then If bBOF = False Then Exit Do Else 'The Beginning of file flag is set to true by default, so when the first ' record is encountered, it is bypassed and the bBOF flag is set to False bBOF = False End If 'If bBOF = Fals End If 'If (iControlRec(0) = 8 OrElse 'Set the default value for the End of Record flag to True ' If the Control Record has all zeros, then it's True, else False bEOR = True 'If the Control record contains all zeros, bEOR will stay True, else it will be set to False For i As Integer = 0 To 5 If iControlRec(i) > 0 Then bEOR = False Exit For End If 'If iControlRec(i) > 0 Next 'For i As Integer = 0 To 5 If bEOR = False Then 'Convert EBCDIC character to ASCII 'Multiply the 6th byte by 6 to get record length ' Why multiply by 6? Because it works. iControlPosSix = EBCDICEncoding.GetBytes(ControlArray(5)) 'If the 4th position of the control record is an 8, then add 6 ' to the record length to pick up remaining characters. If iControlRec(3) = 8 Then iRecordLength = CInt(iControlPosSix(0)) * 6 + 6 Else iRecordLength = CInt(iControlPosSix(0)) * 6 End If 'Add the length of the record to the Consumed Characters counter iConsumedChars += iRecordLength Else 'If the Control Record had all zeros in it, then it is the end of the Block. 'Consume the remainder of the block so we can continue at the beginning of the next block. ReDim chrArray(miBlockLength - iConsumedChars - 1) 'ReDim chrArray(iRecordLength - iConsumedChars - 1) 'Consume (read) the remaining characters in the block. ' We are not doing anything with them because they are not actual records. 'sr.ReadBlock(chrArray, 0, iRecordLength - iConsumedChars) sr.ReadBlock(chrArray, 0, miBlockLength - iConsumedChars) 'Reset the Consumed Characters counter iConsumedChars = 0 'Set the Record Length to 0 so it will not be processed below. iRecordLength = 0 End If ' If bEOR = False End If 'If mbUseControlRec = True If iRecordLength > 0 Then 'Resize our array, dumping previous data. Because Arrays are Zero (0) based, subtract 1 from the Record length. ReDim chrArray(iRecordLength - 1) 'Read the specfied record length, without the Control Record, because we already consumed (read) it. sr.ReadBlock(chrArray, 0, iRecordLength) 'Copy Character Array to String Array, Converting in the process, then Join the Array to a string sRecord = Join(Array.ConvertAll(chrArray, New Converter(Of Char, String)(AddressOf ChrToStr)), "") 'If the record length was 0, then the Join method may return Nothing If IsNothing(sRecord) = False Then If bIgnoreRecord = True Then 'Do nothing - bypass record 'Reset flag bIgnoreRecord = False Else 'Write the line out, LTrimming the specified number of characters. If sRecord.Length >= miLTrim Then sw.WriteLine(sRecord.Remove(0, miLTrim)) Else sw.WriteLine(sRecord.Remove(0, sRecord.Length)) End If ' If sRecord.Length >= miLTrim 'Write out the number of blank lines specified by the 3rd control character. For i As Integer = 1 To iLineSpace - 1 sw.WriteLine("") Next 'For i As Integer = 1 To iLineSpace End If 'If bIgnoreRecord = True 'Obviously, if we have read more characters from the file than the designated size of the block, ' then subtract the number of characters we have read into the next block from the block size. If iConsumedChars > miBlockLength Then 'If iConsumedChars > iRecordLength Then iConsumedChars = iConsumedChars - miBlockLength 'iConsumedChars = iConsumedChars - iRecordLength End If End If 'If IsNothing(sRecord) = False End If 'If iRecordLength > 0 'Allow computer to process (works in a class module, not in a dll) 'Application.DoEvents() Loop 'Destroy StreamReader (sr) sr.Close() sr.Dispose() 'Destroy StreamWriter (sw) sw.Close() sw.Dispose() End Sub '''  ''' Translates 1 EBCDIC Character (Char) to an ASCII String '''  '''  '''  '''  Private Function ChrToStr(ByVal chr As Char) As String Dim sReturn As String = "" 'Convert character into byte Dim EBCDICbyte As Byte() = EBCDICEncoding.GetBytes(chr) 'Convert EBCDIC byte to ASCII byte Dim ASCIIByte As Byte() = Encoding.Convert(EBCDICEncoding, ASCIIEncoding, EBCDICbyte) sReturn = Encoding.ASCII.GetString(ASCIIByte) Return sReturn End Function '''  ''' Translates an EBCDIC String to an ASCII String '''  '''  ''' String '''  Public Function TranslateString(ByVal sStringToTranslate As String) As String Dim i As Integer = 0 Dim sReturn As New System.Text.StringBuilder() 'Loop through the string and translate each character For i = 0 To sStringToTranslate.Length - 1 sReturn.Append(ChrToStr(sStringToTranslate.Substring(i, 1))) Next Return sReturn.ToString() End Function '''  ''' Translates 1 EBCDIC Character (Char) to an ASCII String '''  '''  ''' String '''  Public Function TranslateCharacter(ByVal sCharacterToTranslate As Char) As String Return ChrToStr(sCharacterToTranslate) End Function '''  ''' Translates an EBCDIC Character (Char) Array to an ASCII String '''  '''  ''' String ''' Remarks Public Function TranslateCharacters(ByVal sCharacterArrayToTranslate As Char()) As String Dim sReturn As String = "" 'Copy Character Array to String Array, Converting in the process, then Join the Array to a string sReturn = Join(Array.ConvertAll(sCharacterArrayToTranslate, _ New Converter(Of Char, String)(AddressOf ChrToStr)), "") Return sReturn End Function '''  ''' Block Length must be set. You can set the BlockLength for specific block sizes (Ex: 134). ''' Set UseControlRecord = False for files with specific block sizes (Default is True) '''  ''' 0 ''' Integer '''  Public Property BlockLength() As Integer Get Return miBlockLength End Get Set(ByVal value As Integer) miBlockLength = value End Set End Property '''  ''' Determines whether a ControlKey is used to calculate RecordLength of valid data '''  ''' Default value is True ''' Boolean '''  Public Property UseControlRecord() As Boolean Get Return mbUseControlRec End Get Set(ByVal value As Boolean) mbUseControlRec = value End Set End Property '''  ''' Ignores first record if set (Default is True) '''  ''' Default is True ''' Boolean '''  Public Property IgnoreFirstRecord() As Boolean Get Return mbIgnoreFirstRecord End Get Set(ByVal value As Boolean) mbIgnoreFirstRecord = value End Set End Property '''  ''' Trims the left side of every string the specfied number of characters. Default is 0. '''  ''' Default is 0. ''' Integer '''  Public Property LTrim() As Integer Get Return miLTrim End Get Set(ByVal value As Integer) miLTrim = value End Set End Property End Class 

Algunos enlaces útiles para la traducción de EBCDIC:

Tabla de traducción: útil para verificar algunos de los valores en los campos decimales empaquetados: http://www.simotime.com/asc2ebc1.htm

Lista de páginas de códigos en msdn:
http://msdn.microsoft.com/en-us/library/dd317756(VS.85).aspx

Y un fragmento de código para convertir los campos de la matriz de bytes en C #:

 // 500 is the code page for IBM EBCDIC International System.Text.Encoding enc = new System.Text.Encoding(500); string value = enc.GetString(byteArrayField); 

Los campos empaquetados son los mismos en EBCDIC o ASCII. No ejecute la conversión de EBCDIC a ASCII en ellos. En .Net volcarlos en un byte [].

Usas máscaras y turnos de bits para empacar / desempaquetar. – ¡Pero las operaciones a nivel de bits solo se aplican a los tipos de enteros en .Net, por lo que necesitas saltar algunos aros!

Un buen artista COBOL o C puede apuntarte en la dirección correcta.

Encuentra a uno de los viejos y paga tus cuotas (alrededor de tres cervezas deberían hacerlo).