我有一系列来自大型机的ASCII平面文件,由C#应用程序处理。引入了一个带有Packed Decimal(COMP-3)字段的新Feed,需要将其转换为数值。
使用ASCII传输模式通过FTP传输文件。我担心二进制字段可能包含将被解释为非常低的ASCII代码或控制字符而不是值 - 或者更糟糕的是,可能在FTP过程中丢失。
更重要的是,这些字段被读作字符串。我可以灵活地解决这个问题(即某种流),但业务会给我带来阻力。
要求读作“从HEX转换为ASCII”,但显然没有产生正确的值。任何帮助,将不胜感激;只要您能够解释转换过程的逻辑,它就不必是特定于语言的。
答案 0 :(得分:7)
我一直在观看有关将Comp-3 BCD数据从“传统”大型机文件转换为C#中可用内容的众多主板上的帖子。首先,我想说的是,我对其中一些帖子收到的回复很不满意 - 特别是那些基本上说过“为什么你这些非C#/ C ++相关帖子困扰我们”的回复“以及”如果你需要一个关于某种COBOL约定的答案,你为什么不去参观一个面向COBOL的网站“。对我来说,这是完整的BS,因为很可能需要很多年(不幸的是),软件开发人员需要了解如何处理现实世界中存在的一些遗留问题。所以,即使我在下面的代码中抨击了这篇文章,我将与你分享一个关于COMP-3 / EBCDIC转换我必须处理的真实世界经验(是的,我是他所说的“软盘,纸带,光盘包等...... - 自1979年以来我一直是软件工程师。)。
首先 - 了解您从像IBM这样的传统主框架系统中读取的任何文件将以EBCDIC格式向您呈现数据,并且为了将任何数据转换为您可以处理的C#/ C ++字符串与您将不得不使用正确的代码页转换将数据转换为ASCII格式。如何处理这个问题的一个很好的例子是:
StreamReader readFile = new StreamReader(path,Encoding.GetEncoding(037); // 037 = EBCDIC to ASCII translation。
这将确保您从此流中读取的任何内容都将转换为ASCII并可以以字符串格式使用。这包括COBOL声明的“Zoned Decimal”(图9)和“Text”(Pic X)字段。但是,当读入char []或byte []数组时,这不一定将COMP-3字段转换为正确的“二进制”等效项。要做到这一点,你唯一能够正确翻译(甚至使用UTF-8,UTF-16,默认或其他)代码页的方法,你将要打开这样的文件:
FileStream fileStream = new FileStream(path,FIleMode.Open,FIleAccess.Read,FileShare.Read);
当然,“FileShare.Read”选项是“可选的”。
当您隔离了要转换为十进制值的字段(如果需要,然后是ASCII字符串),您可以使用以下代码 - 这基本上是从MicroSoft“UnpackDecimal”中窃取的你可以得到的帖子:
我已经孤立(我认为)这个逻辑中最重要的部分是什么,并将其合并为两个方法,你可以用你想要的东西。出于我的目的,我选择将其保留为返回十进制值,然后我可以根据自己的需要进行操作。基本上,该方法称为“unpack”,您将其传递给byte []数组(不超过12个字节),并将比例作为int传递,这是您希望在Decimal值中返回的小数位数。我希望这对你有用,对我有用。
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);
}
如果您有任何问题,请在此处发布 - 因为我怀疑我会像其他所有选择发布与当前问题相关的问题的人一样“受到抨击”......
谢谢, 约翰 - 长老。
答案 1 :(得分: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
答案 2 :(得分:3)
如果原始数据在EBCDIC中,则COMP-3字段已出现乱码。 FTP进程已经完成了COMP-3字段中字节值的EBCDIC到ASCII转换,这不是您想要的。要纠正此问题,您可以:
1)使用BINARY模式进行传输,以获得原始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是LEADING(或TRAILING)数字字段。这将数字存储为一串EBCDIC数字,并将符号作为单独的否定( - )或空白字符。所有数字和符号都在FTP传输中正确转换为其ASCII等效值。
答案 3 :(得分: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
答案 4 :(得分:0)
EBCDIC翻译的一些有用链接:
转换表 - 用于检查压缩十进制字段中的某些值: http://www.simotime.com/asc2ebc1.htm
msdn中的代码页列表:
http://msdn.microsoft.com/en-us/library/dd317756(VS.85).aspx
用于转换C#中的字节数组字段的代码:
// 500 is the code page for IBM EBCDIC International
System.Text.Encoding enc = new System.Text.Encoding(500);
string value = enc.GetString(byteArrayField);
答案 5 :(得分:0)
打包字段在EBCDIC或ASCII中是相同的。不要对它们运行EBCDIC到ASCII转换。在.Net中将它们转储为byte []。
您使用按位掩码和shift来打包/解压缩。 - 但是按位操作只适用于.Net中的整数类型,所以你需要跳过一些箍!
优秀的COBOL或C艺术家可以为您指明正确的方向。
找一个老家伙并支付你的会费(约三瓶啤酒应该这样做)。
答案 6 :(得分: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; }
}
}
}
答案 7 :(得分: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;
}
}
}