C#: 将COMP-3压缩十进制转换为人类可读值

6
我有一系列ASCII平面文件从主机传输到C#应用程序进行处理。新的反馈引入了一个需要转换为数值的Packed Decimal (COMP-3)字段。
这些文件是通过FTP传输的,使用ASCII传输模式。我担心二进制字段可能包含被解释为非常低的ASCII代码或控制字符而不是值 - 或者更糟糕的是,在FTP过程中可能会丢失。
此外,这些字段被读取为字符串。我可能有灵活性来解决这个问题(例如某种流),但业务将给我推回。
要求的内容是“从十六进制转换为ASCII”,但显然这没有产生正确的值。任何帮助都将不胜感激; 只要您能解释转换过程的逻辑,它就不必是特定于语言的。
8个回答

10
我一直在关注各种板块上有关将Comp-3 BCD数据从“传统”的大型机文件转换为C#中可用的内容。首先,我想说的是,我对某些帖子收到的回复并不感到满意,尤其是那些基本上说“为什么要用这些非C#/ C ++相关的帖子来烦我们”以及“如果你需要了解某种COBOL约定的答案,为什么不去访问一个COBOL导向的网站”。对我来说,这完全是胡说八道,因为在现实世界中,软件开发人员将需要理解如何处理存在于“真实世界”中的某些遗留问题,而这种需求可能会持续很多年(不幸的是)。因此,即使我因以下代码而在这篇文章中受到抨击,我也将与您分享我在处理COMP-3 / EBCDIC转换方面遇到的真实经验(是的,我是谈论“软盘,纸带,磁盘包等等”的人 - 自1979年以来我一直是一名软件工程师)。
首先,需要了解从类似IBM的遗留主机系统中读取的任何文件都会以EBCDIC格式呈现数据,为了将这些数据转换为您可以处理的C# / C ++字符串,您需要使用适当的代码页翻译将数据转换为ASCII格式。一个很好的处理方法是:
StreamReader readFile = new StreamReader(path, Encoding.GetEncoding(037); // 037 = EBCDIC to ASCII translation.
这将确保您从此流中读取的任何内容都将被转换为ASCII并可用于字符串格式。这包括COBOL声明的“分区十进制”(Pic 9)和“文本”(Pic X)字段。但是,当读入char[]或byte[]数组时,这并不一定将COMP-3字段转换为正确的“二进制”等效项。要做到这一点,即使使用UTF-8,UTF-16,Default或其他任何代码页,您也必须以以下方式打开文件:
FileStream fileStream = new FileStream(path, FIleMode.Open, FIleAccess.Read, FileShare.Read);
当然,“FileShare.Read”选项是“可选的”。
当您已经隔离出想要转换为十进制值(如果需要,随后再转换为ASCII字符串)的字段时,您可以使用以下代码-这基本上是从MicroSoft的“UnpackDecimal”发布中窃取的,您可以在以下网址获取该发布:

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

我认为我已经将这个逻辑中最重要的部分分离出来,并将其整合成了两个方法,您可以自由使用。对于我的目的,我选择将其返回为十进制值,然后再进行操作。基本上,这个方法被称为“解包”,您需要将一个byte[]数组(不超过12个字节)和一个int类型的比例传递给它,这个比例是您想在十进制值中返回的小数位数。希望这个方法能像我一样对您有用。
    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);
    }

如果您有任何问题,请在此处发布-因为我怀疑我会像其他人一样被“烧毁”,他们选择发布与今天的问题相关的问题...谢谢,John - The Elder.

如何读取文件并将其传递给Unpack,我正在使用filestream打开文件,然后使用StreamReader读取每行,然后将该行转换为字节以传递给Unpack方法,直到流结束。byte[] b = ASCIIEncoding.ASCII.GetBytes(reader.ReadLine()); Unpack(b,0);但是在转换13位二进制格式数字时它会给出错误的值,如果我做错了什么,请您指导一下如何读取文件并将其转换为字节并传递给unpack。谢谢 - VR1256

6
首先,您必须消除ASCII传输模式引起的行尾(EOL)转换问题。当BCD值恰好对应于EOL字符时,您绝对正确地关注数据损坏问题。这个问题最糟糕的方面是它会偶尔和出乎意料地发生。
最好的解决方案是将传输模式更改为BIN。这是适当的,因为您正在传输的数据是二进制的。如果无法使用正确的FTP传输模式,则可以在代码中撤消ASCII模式的损坏。您只需将\r\n对转换回\n即可。如果我是您,我会确保这得到了很好的测试。
一旦解决了EOL问题,COMP-3转换就非常简单了。我能够在MS知识库中找到this article,其中包含BASIC示例代码。请参见下面的VB.NET版本。

由于涉及到COMP-3值,您正在读取的文件格式几乎肯定具有固定记录大小和固定字段长度。如果我是您,我会在继续进行之前获取文件格式规范。您应该使用BinaryReader来处理此数据。如果有人对此提出反对意见,我会离开。让他们找别人来满足他们的愚蠢。

这是BASIC示例代码的VB.NET版本。由于我无法访问COMP-3文件,因此我尚未测试此代码。如果这不起作用,我会参考原始的MS示例代码以获得指导,或参考其他答案中关于此问题的参考资料。

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

二进制传输模式会对包含ASCII数据的文件的其余部分造成任何问题吗?上游过程将取消任何看起来比预定长度短的行,因此我可能能够反击并将其更改为纯文本数字。谢谢。 - tsilb
实际上,那段代码是用QBasic写的...我知道这一点,但已经有大约10年没有使用过了 :) 我太生疏了,无法将其转换为C#,更不用说如何在ASCII文件中使用它来处理8个字符的二进制字段...但还是谢谢你的尝试。 - tsilb
二进制 FTP 传输不会改变文件。 - idstam
如果这是被接受的答案,你能告诉我们你最终是如何让它工作的吗?!我正在寻找解决完全相同问题的方法,最终来到了这里。请帮帮我!!!我不懂QBasic,我太年轻了。 - Josh Stodola
我已经添加了一个VB.NET端口。我不确定这有多大帮助,但将旧的QBASIC代码移植到VB.NET 3.5是一项有趣的练习。令人惊讶的是,只需要很少的更改就可以使其编译和运行。但是,我会对输入语句保持警惕,我不能保证它们会正确地进行二进制读取。如果您有适当的测试文件,应该能够验证代码的正确性(或不正确性)。 - Paul Keister

3
如果原始数据是EBCDIC编码,那么COMP-3字段已经被破坏了。FTP过程已经将COMP-3字段中的字节值从EBCDIC转换为ASCII,这不是你想要的结果。为了纠正这个问题,你可以:
1)使用二进制模式传输,以获取原始的EBCDIC数据。然后将COMP-3字段转换为数字,并将记录中的任何其他EBCDIC文本转换为ASCII。打包字段将每个数字存储在半字节中,其中较低的半字节作为符号(F为正值,其他值通常为D或E为负)。在PIC 999.99 USAGE COMP-3中存储123.4将是X'01234F'(三个字节),而在同一字段中存储-123将是X'01230D'。
2)让发送方将该字段转换为USAGE IS DISPLAY SIGN IS LEADING(或TRAILING)数字字段。这将把数字存储为EBCDIC数字字符的字符串,符号作为单独的负数(-)或空格字符。所有数字和符号在FTP传输时都正确地转换为它们的ASCII等效项。

谢谢,我们已经处理了。上游依赖项已更改平面文件以提供可读格式的数字。 - tsilb
隐含小数怎么办?就像这样:PIC 999v99 COMP-3 - Josh Stodola
隐含意味着小数点不在存储的值中。使用PIC 999.99 USAGE IS DISPLAY SIGN LEADING,您可以获得-12345字符串以表示-123.45值,或b12345(前导空格)以表示123.45正值。当进行从EBCDIC到ASCII的FTP时,数字文本会被正确转换。 - Paul Morgan

2

如果我偏离了主题,我很抱歉,但也许我在这里粘贴的代码示例可以帮助你。这段代码来自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



''' <summary>
''' Translates an EBCDIC file to an ASCII file.
''' </summary>
''' <remarks></remarks>
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

    ''' <summary>
    ''' 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.
    ''' </summary>
    ''' <param name="SourceFile">Enter the path of the Source File.</param>
    ''' <param name="DestinationFile">Enter the path of the Destination File.</param>
    ''' <remarks></remarks>
    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



    ''' <summary>
    ''' Translates 1 EBCDIC Character (Char) to an ASCII String
    ''' </summary>
    ''' <param name="chr"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    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



    ''' <summary>
    ''' Translates an EBCDIC String to an ASCII String
    ''' </summary>
    ''' <param name="sStringToTranslate"></param>
    ''' <returns>String</returns>
    ''' <remarks></remarks>
    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



    ''' <summary>
    ''' Translates 1 EBCDIC Character (Char) to an ASCII String
    ''' </summary>
    ''' <param name="sCharacterToTranslate"></param>
    ''' <returns>String</returns>
    ''' <remarks></remarks>
    Public Function TranslateCharacter(ByVal sCharacterToTranslate As Char) As String

        Return ChrToStr(sCharacterToTranslate)

    End Function



    ''' <summary>
    ''' Translates an EBCDIC Character (Char) Array to an ASCII String
    ''' </summary>
    ''' <param name="sCharacterArrayToTranslate"></param>
    ''' <returns>String</returns>
    ''' <remarks>Remarks</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


    ''' <summary>
    ''' 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)
    ''' </summary>
    ''' <value>0</value>
    ''' <returns>Integer</returns>
    ''' <remarks></remarks>
    Public Property BlockLength() As Integer
        Get
            Return miBlockLength

        End Get
        Set(ByVal value As Integer)
            miBlockLength = value

        End Set
    End Property



    ''' <summary>
    ''' Determines whether a ControlKey is used to calculate RecordLength of valid data
    ''' </summary>
    ''' <value>Default value is True</value>
    ''' <returns>Boolean</returns>
    ''' <remarks></remarks>
    Public Property UseControlRecord() As Boolean
        Get
            Return mbUseControlRec

        End Get
        Set(ByVal value As Boolean)
            mbUseControlRec = value

        End Set
    End Property



    ''' <summary>
    ''' Ignores first record if set (Default is True)
    ''' </summary>
    ''' <value>Default is True</value>
    ''' <returns>Boolean</returns>
    ''' <remarks></remarks>
    Public Property IgnoreFirstRecord() As Boolean
        Get
            Return mbIgnoreFirstRecord

        End Get

        Set(ByVal value As Boolean)
            mbIgnoreFirstRecord = value

        End Set
    End Property



    ''' <summary>
    ''' Trims the left side of every string the specfied number of characters.  Default is 0.
    ''' </summary>
    ''' <value>Default is 0.</value>
    ''' <returns>Integer</returns>
    ''' <remarks></remarks>
    Public Property LTrim() As Integer
        Get
            Return miLTrim

        End Get

        Set(ByVal value As Integer)
            miLTrim = value

        End Set
    End Property


End Class

哇,谢谢。我不会因为你的大粘贴而扣分(毕竟这是一个答案网站)...给你点赞,因为你的回答快速并且看起来正确 - 但我要等到周一回去上班才能接受它。幸运的是我也懂VB.NET...谢谢。 - tsilb
不行,那样做没用。无论是纯文本数字还是COMP-3数据,它都会输出一堆垃圾。根据我的示例数据,我需要将“<”转换为“4C”,将“b”转换为“82”,将“2”转换为“F2”,将“0”转换为“F0”。谢谢你的尝试。 - tsilb

0

在EBCDIC或ASCII中,压缩字段是相同的。不要对它们运行EBCDIC到ASCII转换。在.Net中将它们转储到byte[]。

您可以使用位掩码和移位来打包/解包。 -- 但是,在.Net中,位运算仅适用于整数类型,因此您需要通过一些技巧来实现!

一个优秀的COBOL或C艺术家可以指引您正确的方向。

找到其中一位老手并付出您的代价(大约三杯啤酒就可以了)。


0

0

“ASCII传输类型”会将文件作为常规文本文件传输。因此,在ASCII传输类型下传输压缩的十进制或二进制数据文件时,文件容易损坏。 “二进制传输类型”会以二进制模式传输数据,将文件处理为二进制数据而不是文本数据。因此,我们必须在这里使用二进制传输类型。 参考:https://www.codeproject.com/Tips/673240/EBCDIC-to-ASCII-Converter

一旦您的文件准备好,这里有一个将压缩的十进制转换为可读十进制的代码。

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

   namespace ConsoleApp2
   {
    class Program
    {
        static void Main(string[] args)
        {
            var path = @"C:\FileName.BIN.dat";
            var templates = new List<Template>
            {
                new Template{StartPos=1,CharLength=4,Type="AlphaNum"},
                new Template{StartPos=5,CharLength=1,Type="AlphaNum"},
                new Template{StartPos=6,CharLength=8,Type="AlphaNum"},
                new Template{StartPos=14,CharLength=1,Type="AlphaNum"},
                new Template{StartPos=46,CharLength=4,Type="Packed",DecimalPlace=2},
                new Template{StartPos=54,CharLength=5,Type="Packed",DecimalPlace=0},
                new Template{StartPos=60,CharLength=4,Type="Packed",DecimalPlace=2},
                new Template{StartPos=64,CharLength=1,Type="AlphaNum"}
            };

            var allBytes = File.ReadAllBytes(path);
            for (int i = 0; i < allBytes.Length; i += 66)
            {
                var IsLastline = (allBytes.Length - i) < 66;
                var lineLength = IsLastline ? 64 : 66;
                byte[] lineBytes = new byte[lineLength];
                Array.Copy(allBytes, i, lineBytes, 0, lineLength);


                var outArray = new string[templates.Count];
                int index = 0;
                foreach (var temp in templates)
                {
                    byte[] amoutBytes = new byte[temp.CharLength];
                    Array.Copy(lineBytes, temp.StartPos - 1, amoutBytes, 0, 
    temp.CharLength);
                    var final = "";
                    if (temp.Type == "Packed")
                    {
                        final = Unpack(amoutBytes, temp.DecimalPlace).ToString();
                    }
                    else
                    {
                        final = ConvertEbcdicString(amoutBytes);
                    }

                    outArray[index] = final;
                    index++;

                }

                Console.WriteLine(string.Join(" ", outArray));

            }

            Console.ReadLine();
        }


        private static string ConvertEbcdicString(byte[] ebcdicBytes)
        {
            if (ebcdicBytes.All(p => p == 0x00 || p == 0xFF))
            {
                //Every byte is either 0x00 or 0xFF (fillers)
                return string.Empty;
            }

            Encoding ebcdicEnc = Encoding.GetEncoding("IBM037");
            string result = ebcdicEnc.GetString(ebcdicBytes); // convert EBCDIC Bytes -> 
    Unicode string
            return result;
        }

        private static 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, AlphaNumd "F" hex is unsigned. 
            var ff = nibble(inp, 0);
            switch (ff)
            {
                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 static int nibble(byte[] inp, int nibbleNo)
        {
            int b = inp[inp.Length - 1 - nibbleNo / 2];
            return (nibbleNo % 2 == 0) ? (b & 0x0000000F) : (b >> 4);
        }

        class Template
        {
            public string Name { get; set; }
            public string Type { get; set; }
            public int StartPos { get; set; }
            public int CharLength { get; set; }
            public int DecimalPlace { get; set; }
        }
    }
   }

0

文件必须以二进制方式传输。以下是更简短的方法:

using System.Linq;

namespace SomeNamespace
{
    public static class SomeExtensionClass
    {
        /// <summary>
        /// computes the actual decimal value from an IBM "Packed Decimal" 9(x)v9 (COBOL) format
        /// </summary>
        /// <param name="value">byte[]</param>
        /// <param name="precision">byte; decimal places, default 2</param>
        /// <returns>decimal</returns>
        public static decimal FromPackedDecimal(this byte[] value, byte precision = 2)
        {
            if (value.Length < 1)
            {
                throw new System.InvalidOperationException("Cannot unpack empty bytes.");
            }
            double power = System.Math.Pow(10, precision);
            if (power > long.MaxValue)
            {
                throw new System.InvalidOperationException(
                    $"Precision too large for valid calculation: {precision}");
            }
            string hex = System.BitConverter.ToString(value).Replace("-", "");
            var bytes = Enumerable.Range(0, hex.Length)
                     .Select(x => System.Convert.ToByte($"0{hex.Substring(x, 1)}", 16))
                     .ToList();
            long place = 1;
            decimal ret = 0;
            for (int i = bytes.Count - 2; i > -1; i--)
            {
                ret += (bytes[i] * place);
                place *= 10;
            }
            ret /= (long)power;
            return (bytes.Last() & (1 << 7)) != 0 ? ret * -1 : ret;
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接