65.9K
CodeProject 正在变化。 阅读更多。
Home

改编 JSON 字符串以反序列化为 C# 对象

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2019 年 6 月 23 日

CPOL

11分钟阅读

viewsIcon

12073

某些 JSON 字符串需要一些帮助才能放入 C# 对象中。

引言

最近有两个项目都围绕着将 REST API 端点返回的 JSON 字符串反序列化为 C# 类的实例。虽然第一个项目中的大多数反序列化都很常规,但该项目中的一项,以及最近项目中的所有反序列化。包含阻止其直接用作诸如 Newtonsoft.Json.JsonConvert.DeserializeObject<T> 之类的 JSON 反序列化器的输入的文本。对于第一个项目,我设计了一个字符串扩展方法来对字符串应用一对对替换。对于最近的项目,我进一步对其进行了改进,并添加了另一个执行更复杂字符串替换集的方法。

背景

要适合反序列化为 C# 对象,JSON 字符串应如下面的片段所示。

{
    "Meta_Data": {

        "Information": "Daily Time Series with Splits and Dividend Events",
        "Symbol": "BA",
        "LastRefreshed": "2019-05-08 16:00:44",
        "OutputSize": "Compact",
        "TimeZone": "US/Eastern"
    },

    "Time_Series_Daily" : [
    {
        "Activity_Date": "2019-05-08",
        "Open": "357.7700",
        "High": "361.5200",
        "Low": "353.3300",
        "Close": "359.7500",
        "AdjustedClose": "359.7500",
        "Volume": "5911593",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
    },
    {
        "Activity_Date": "2019-05-07",
        "Open": "366.3300",
        "High": "367.7100",
        "Low": "355.0200",
        "Close": "357.2300",
        "AdjustedClose": "357.2300",
        "Volume": "9733702",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
    },
    {
        "Activity_Date": "2019-05-06",
        "Open": "367.8800",
        "High": "372.4800",
        "Low": "365.6300",
        "Close": "371.6000",
        "AdjustedClose": "371.6000",
        "Volume": "4747601",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
    },

…

    {
        "Activity_Date": "2018-12-14",
        "Open": "322.4500"
        "High": "323.9100"
        "Low": "315.5600",
        "Close": "318.7500",
        "AdjustedClose": "317.1415",
        "Volume": "3298436",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
     },
        "Activity_Date": "2018-12-13"
        "Open": "328.4000",
        "High": "328.7400",
        "Low": "324.1730",
        "Close": "325.4700",
        "AdjustedClose": "323.8276",
        "Volume": "2247706",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
     }
   ]
}

此片段表示一个根对象和两个子对象。

  1. Meta_Data 是一个标量对象,具有五个属性,每个属性由一个名称-值对表示。
  2. Time_Series_Daily 是一个数组,其中包含任意数量的标量对象,每个对象都有九个属性,也由名称-值对表示。

REST API 端点返回的 JSON 字符串如下面的片段所示。

{
   "Meta Data": {
      "1. Information": "Daily Time Series with Splits and Dividend Events",
      "2. Symbol": "BA",
      "3. Last Refreshed": "2019-05-08 16:00:44",
      "4. Output Size": "Compact",
      "5. Time Zone": "US/Eastern"
   },
      "Time Series (Daily)": {
           "2019-05-08": {
               "1. open": "357.7700",
               "2. high": "361.5200",
               "3. low": "353.3300",
               "4. close": "359.7500",
               "5. adjusted close": "359.7500",
               "6. volume": "5911593",
               "7. dividend amount": "0.0000",
               "8. split coefficient": "1.0000"
           },
           "2019-05-07": {
               "1. open": "366.3300",
               "2. high": "367.7100",
               "3. low": "355.0200",
               "4. close": "357.2300",
               "5. adjusted close": "357.2300",
               "6. volume": "9733702",
               "7. dividend amount": "0.0000",
               "8. split coefficient": "1.0000"
           },
…

           "2018-12-14": {
               "1. open": "322.4500",
               "2. high": "323.9100",
               "3. low": "315.5600",
               "4. close": "318.7500",
               "5. adjusted close": "317.1415",
               "6. volume": "3298436",
               "7. dividend amount": "0.0000",
               "8. split coefficient": "1.0000"
            },
            "2018-12-13": {
               "1. open": "328.4000",
               "2. high": "328.7400",
               "3. low": "324.1730",
               "4. close": "325.4700",
               "5. adjusted close": "323.8276",
               "6. volume": "2247706",
               "7. dividend amount": "0.0000",
               "8. split coefficient": "1.0000"
           }
     ]
}

上面显示的 JSON 字符串有几处问题。

  1. 大多数对象和属性名称包含嵌入式空格、括号、句点和其他在 C# 变量名称中无效的字符。
  2. 将要成为 Time_Series_Daily 数组的 Time Series (Daily) 元素,组织成 Time_Series_Daily 对象上的一组命名属性,其中每个属性都是一个标量对象,该对象带有八个属性。
  3. 这八个属性的名称以数字开头,这对于 C# 变量名称的第一个字符是无效的。

即使您愿意接受动态生成的对象,处理该对象也会很麻烦。数据集是时间序列的股票市场数据,这使得人们希望将其视为数据点的数组。通过一些转换,其中大部分都很简单,REST 端点返回的字符串可以转换为易于反序列化为数组的内容,从而可以非常有效地进行处理。

解决此难题的另一个重要知识是,Visual Studio 代码编辑器的 **“选择性粘贴”** 功能可以将格式良好的 XML 或 JSON 转换为类定义。

  1. 将 JSON(或 XML)复制到 Windows 剪贴板。
  2. 使用解决方案资源管理器定义一个新类。
  3. 使用 **编辑** 菜单上的 **“选择性粘贴”** 工具将字符串粘贴到空类中,替换整个类定义。保留命名空间块。
  4. 编辑器将最外层元素(类)命名为 RootObject。将其重命名为与您在创建空类时为其指定的名称匹配。虽然如果您仅通过此技术创建一个类就可以不重命名根对象,但保留它会产生糟糕的代码味道。

使用代码

本文附带的演示包是 GitHub 存储库,网址为 https://github.com/txwizard/LineBreakFixupsDemo/。有关设置和使用代码的说明,请参阅 Windows 到 Unix 再到 Windows 的换行符 中同名的部分,我建议您严格按照说明操作,即使您打算在 Visual Studio 调试器中运行它。我建议您严格按照说明操作的原因是,尽管可以从 NuGet Gallery 获取依赖项,并且可以从提供的源构建项目,但必须按照指示从 ZIP 文件中提取测试数据文件,以便演示程序在那里找到它们。

您可以从命令提示符、**运行**框、文件资源管理器或 Visual Studio 调试器运行该程序,也可以使用命令行参数 TransformJSONString 来单独运行本文讨论的 JSON 转换演示。该参数可以附加到命令提示符窗口或运行框中的命令字符串,也可以添加到 Visual Studio 项目属性编辑器的 **调试** 选项卡中。

关注点

转换分两步进行(第三步是前面提到的换行符修复,请参阅 Windows 到 Unix 再到 Windows 的换行符)。静态方法 PerformJSONTransofmration(如下所示,并在 Program.cs 中定义)负责管理转换、反序列化和生成报告,该报告列出了时间序列数据点。

private static int PerformJSONTransofmration (
    int pintTestNumber ,
    bool pfConvertLineEndings ,
    string pstrTestReportLabel ,
    string pstrRESTResponseFileName ,
    string pstrIntermediateFileName ,
    string pstrFinalOutputFileName ,
    string pstrResponseObjectFileName )
{
    Utl.BeginTest (
        pstrTestReportLabel ,
        ref pintTestNumber );
    string strRawResponse = pfConvertLineEndings
        ? Utl.GetRawJSONString ( pstrRESTResponseFileName ).UnixLineEndings ( )
        : Utl.GetRawJSONString ( pstrRESTResponseFileName );
    JSONFixupEngine engine = new JSONFixupEngine ( @"TIME_SERIES_DAILY_ResponseMap" );
    string strFixedUp_Pass_1 = engine.ApplyFixups_Pass_1 ( strRawResponse );
    Utl.PreserveResult (
        strFixedUp_Pass_1                                   // string pstrPreserveThisResult
        pstrIntermediateFileName ,                          // string pstrOutputFileNamePerSettings
        Properties.Resources.FILE_LABEL_INTERMEDIATE );     // string pstrLabelForReportMessage
    string strFixedUp_Pass_2 = engine.ApplyFixups_Pass_2 ( strFixedUp_Pass_1 );
    Utl.PreserveResult (
        strFixedUp_Pass_2 ,                                 // string pstrPreserveThisResult
        pstrFinalOutputFileName ,                           // string pstrOutputFileNamePerSettings
        Properties.Resources.FILE_LABEL_FINAL );            // string pstrLabelForReportMessage
    //  ------------------------------------------------------------
    // TimeSeriesDailyResponse<
    //  ------------------------------------------------------------

    Utl.ConsumeResponse (
        pstrResponseObjectFileName ,
        Newtonsoft.Json.JsonConvert.DeserializeObject<TimeSeriesDailyResponse> (
            strFixedUp_Pass_2 ) );
    s_smThisApp.BaseStateManager.AppReturnCode = Utl.TestDone (
        MagicNumbers.ERROR_SUCCESS ,
        pintTestNumber );
    return pintTestNumber;
}    // private static int PerformJSONTransofmration

此方法大部分是顺序代码。

  1. 静态方法 Utl.GetRawJSONString 将 REST 端点返回的 JSON 响应从文本文件读入字符串。如果输入文件经过了任何过程(例如 Git 客户端),该过程可能已将预期的 Unix 换行符替换为 Windows 换行符,则 UnixLineEndings 扩展方法将链接到该方法,以确保 strRawResponse 具有 Unix 换行符。在生产应用程序中,我期望不做任何假设,并始终调用 UnixLineEndings
  2. 将构造一个新的 JSONFixupEngine 对象。其字符串参数 @"TIME_SERIES_DAILY_ResponseMap" 是一个嵌入式文本文件资源的名称,其中包含字符串替换对列表,我稍后将对此进行解释。
  3. 实例方法 ApplyFixups_Pass_1strRawResponse 为输入,并返回 strFixedUp_Pass_1 (喜欢这些富有想象力的名字,不是吗?)。此方法包装了对私有实例成员 _responseStringFixups 上的 ApplyFixups 方法的调用,该成员是一个 StringFixups 对象,由构造函数从嵌入式文本文件资源 TIME_SERIES_DAILY_ResponseMap.TXT 中读取的替换对创建。然后将此对象传递给 StringFixups 构造函数,最后在 strRawResponse 的字符串参数上调用 ApplyFixups
  4. 实例方法 ApplyFixups_Pass_2strFixedUp_Pass_1 为输入,返回 strFixedUp_Pass_2
  5. 静态方法 Utl.ConsumeResponse 采用字符串参数 pstrResponseObjectFileName(要分配给制表符分隔的报告文件的名称),以及通过将字符串 strFixedUp_Pass_2 馈送到静态方法 Newtonsoft.Json.JsonConvert.DeserializeObject<T> 返回的 TimeSeriesDailyResponse 对象。此方法不返回任何值。

在上述每个步骤之间,静态 void 方法 Utl.PreserveResult 将刚刚创建的输出字符串写入新的文本文件,然后将文件名和其他统计信息打印到控制台日志。

ApplyFixups_Pass_1:第一次转换

第一次转换使用 **表 1** 中显示的字符串替换对,JSONFixupEngine 构造函数将这些字符串对存储在 StringFixups.StringFixup 结构的数组中,该数组由嵌入式文本文件资源构建并馈送到 StringFixups 构造函数。最终,StringFixups 是一个 StringFixup 结构数组的容器,它通过其自己的 ApplyFixups 方法应用于其字符串参数的同名扩展方法。

StringFixups 构造函数值得注意的是它使用了 LoadStringFixups(如下所示),后者又结合了 WizardWrx.EmbeddedTextFile.Readers 上的静态方法 LoadTextFileFromEntryAssembly 和一个 WizardWrx.AnyCSV.Parser 实例,将嵌入式文本文件拆分为 StringFixups.StringFixup 结构的数组。

private static StringFixups.StringFixup [ ] LoadStringFixups ( string pstrEmbeddedResourceName )
{
    const string LABEL_ROW = @"JSON     VS";
    const string TSV_EXTENSION = @".txt";

    const int STRING_PER_RESPONSE = ArrayInfo.ARRAY_FIRST_ELEMENT;
    const int STRING_FOR_JSONCONVERTER = STRING_PER_RESPONSE + ArrayInfo.NEXT_INDEX;
    const int EXPECTED_FIELD_COUNT = STRING_FOR_JSONCONVERTER + ArrayInfo.NEXT_INDEX;

    string strEmbeddResourceFileName = string.Concat (
        pstrEmbeddedResourceName ,
        TSV_EXTENSION );

    string [ ] astrAllMapItems = Readers.LoadTextFileFromEntryAssembly ( strEmbeddResourceFileName );
    Parser parser = new Parser (
        CSVParseEngine.DelimiterChar.Tab ,
        CSVParseEngine.GuardChar.DoubleQuote ,
        CSVParseEngine.GuardDisposition.Strip );
    StringFixups.StringFixup [ ] rFunctionMaps = new StringFixups.StringFixup [ ArrayInfo.IndexFromOrdinal ( astrAllMapItems.Length ) ];

    for ( int intI = ArrayInfo.ARRAY_FIRST_ELEMENT ;
              intI < astrAllMapItems.Length ;
              intI++ )
    {
        if ( intI == ArrayInfo.ARRAY_FIRST_ELEMENT )
        {
            if ( astrAllMapItems [ intI ] != LABEL_ROW )
            {
                throw new Exception (
                    string.Format (
                        Properties.Resources.ERRMSG_CORRUPTED_EMBBEDDED_RESOURCE_LABEL ,
                        new string [ ]
                        {
                            strEmbeddResourceFileName ,     // Format Item 0: internal resource {0}
                            LABEL_ROW ,                     // Format Item 1: Expected value = {1}
                            astrAllMapItems [ intI ] ,      // Format Item 2: Actual value   = {2}
                            Environment.NewLine             // Format Item 3: Platform-specific newline
                        } ) );
            }   // if ( astrAllMapItems[intI] != LABEL_ROW )
        }   // TRUE (label row sanity check 1 of 2) block, if ( intI == ArrayInfo.ARRAY_FIRST_ELEMENT )
        else
        {
            string [ ] astrFields = parser.Parse ( astrAllMapItems [ intI ] );

            if ( astrFields.Length == EXPECTED_FIELD_COUNT )
            {
                rFunctionMaps [ ArrayInfo.IndexFromOrdinal ( intI ) ] = new StringFixups.StringFixup (
                    astrFields [ STRING_PER_RESPONSE ] ,
                    astrFields [ STRING_FOR_JSONCONVERTER ] );
            }   // TRUE (anticipated outcome) block, if ( astrFields.Length == EXPECTED_FIELD_COUNT )
            else
            {
                throw new Exception (
                    string.Format (
                        Properties.Resources.ERRMSG_CORRUPTED_EMBEDDED_RESOURCE_DETAIL ,
                        new object [ ]
                        {
                            intI ,                          // Format Item 0: Detail record {0}
                            strEmbeddResourceFileName ,     // Format Item 1: internal resource {1}
                            EXPECTED_FIELD_COUNT ,          // Format Item 2: Expected field count = {2}
                            astrFields.Length ,             // Format Item 3: Actual field count   = {3}
                            astrAllMapItems [ intI ] ,      // Format Item 4: Actual record        = {4}
                            Environment.NewLine             // Format Item 5: Platform-specific newline
                        } ) );
            }   // FALSE (unanticipated outcome) block, if ( astrFields.Length == EXPECTED_FIELD_COUNT )
        }   // FALSE (detail row) block, if ( intI == ArrayInfo.ARRAY_FIRST_ELEMENT )
    }   // for ( int intI = ArrayInfo.ARRAY_FIRST_ELEMENT ; intI < astrAllMapItems.Length ; intI++ )

    return rFunctionMaps;
}   // private static StringFixups.StringFixup [ ] GetSStringFixups

为防止数据损坏,LoadStringFixups 对输入文件中的标签行和每个详细信息行进行健全性检查。如果出现任何问题,将引发异常,并应将其捕获和报告,因为附带的消息提供了大量详细信息,旨在查明损坏的来源。

关于我选择将文件嵌入程序集,这少了一件需要管理的事情。文件位于源代码文件夹中,属于该项目,并被标记为 **嵌入式资源** 内容。每次构建项目时,文本文件都会复制到程序集中,因此程序在执行时始终可用。

ArrayInfo.ARRAY_FIRST_ELEMENT ArrayInfo.NEXT_INDEX,两者都定义在 WizardWrx.Common.dll 中,并导出到根 WizardWrx 命名空间,以及由 WizardWrx.AnyCSV.dll 导出的多个常量,用于初始化 LoadStringFixups 定义和使用的本地常量。这些只是众多常量中的一部分,其中大多数属于 WizardWrx.Common.dll 导出到根 WizardWrx 命名空间的众多静态类,WizardWrx.Common.dll 可作为 NuGet 包 WizardWrx.Common 获得。除了这些常量之外,WizardWrx.Common.dll 中定义的托管字符串资源被标记为 **Public**,因此任何程序集都可以使用它们。

这些常量和公共字符串是巨大的节省劳力的工具,更不用说它们在代码可读性方面带来的改进了。

1 列出了字符串替换对。左列标记为 **JSON** 的字符串是 REST 端点返回的字符串。右列标记为 **VS** 的字符串是替换它们的有效变量名。

JSON

VS

Meta Data

Meta_Data

1. Information

信息

2. Symbol

符号

3. Last Refreshed

LastRefreshed

4. Output Size

OutputSize

5. Time Zone

TimeZone

Time Series (Daily)

TimeSeriesDaily

1. open

打开

2. high

3. low

低功耗

4. close

Close

5. adjusted close

AdjustedClose

6. volume

7. dividend amount

DividendAmount

8. split coefficient

SplitCoefficient

字符串扩展方法 ApplyFixups 是一个简单的 for 循环,它迭代传递给它的 StringFixup 数组,依次将每个元素应用于输入字符串。第一次迭代从参数 pstrIn 初始化输出字符串 rstrFixedUp,后续迭代以此作为输入,返回一个新的 rstrFixedUp 字符串。

public static string ApplyFixups (
    this string pstrIn ,
    WizardWrx.Core.StringFixups.StringFixup [ ] pafixupPairs )
{
    string rstrFixedUp = null;

    for ( int intFixupIndex = ArrayInfo.ARRAY_FIRST_ELEMENT ;
              intFixupIndex < pafixupPairs.Length ;
              intFixupIndex++ )
    {
        if ( intFixupIndex == ArrayInfo.ARRAY_FIRST_ELEMENT )
        {
            rstrFixedUp = pstrIn.Replace (
                pafixupPairs [ intFixupIndex ].InputValue ,
                pafixupPairs [ intFixupIndex ].OutputValue );
        }   // TRUE (On the first pass, the output string is uninitialized.) block, if ( intFixupIndex == ArrayInfo.ARRAY_FIRST_ELEMENT )
        else
        {
            rstrFixedUp = rstrFixedUp.Replace (
                pafixupPairs [ intFixupIndex ].InputValue ,
                pafixupPairs [ intFixupIndex ].OutputValue );
        }   // FALSE (Subsequent passes must feed the output string through its Replace method with the next StringFixup pair.) block, if ( intFixupIndex == ArrayInfo.ARRAY_FIRST_ELEMENT )
    }   // for ( int intFixupIndex = ArrayInfo.ARRAY_FIRST_ELEMENT ; intFixupIndex < _afixupPairs.Length ; intFixupIndex++ )

    return rstrFixedUp;
}   // ApplyFixups method

ApplyFixups_Pass_2:第二次转换

JSONFixupEngine 实例方法 ApplyFixups_Pass_2 执行字符串转换的第二阶段,它有点不那么直接,因为负责大部分处理的中央 while 循环的第一次迭代使用布尔状态标志属性 _fIsFirstPass 来改变其后续迭代的行为。整个方法体如下所示。

public string ApplyFixups_Pass_2 ( string pstrFixedUp_Pass_1 )
{   // This method references and updates instance member __fIsFirstPass.
    const string TSD_LABEL_ANTE = "\"TimeSeriesDaily\": {"		// Ante: "TimeSeriesDaily": {
	const string TSD_LABEL_POST = "\"Time_Series_Daily\" : ["	// Post: "Time_Series_Daily": [

	const string END_BLOCK_ANTE = "}\n    }\n}";
	const string END_BLOCK_POST = "}\n    ]\n}";

	const int DOBULE_COUNTING_ADJUSTMENT = MagicNumbers.PLUS_ONE;					// Deduct one from the length to account for the first character occupying the position where copying begins.

	_fIsFirstPass = true															// Re-initialize the First Pass flag.

	StringBuilder builder1 = new StringBuilder ( pstrFixedUp_Pass_1.Length * MagicNumbers.PLUS_TWO );

	builder1.Append (
		pstrFixedUp_Pass_1.Replace (
		TSD_LABEL_ANTE ,
		TSD_LABEL_POST ) );
	int intLastMatch = builder1.ToString ( ).IndexOf ( TSD_LABEL_POST )
		+ TSD_LABEL_POST.Length
		- DOBULE_COUNTING_ADJUSTMENT;

	while ( intLastMatch > ListInfo.INDEXOF_NOT_FOUND )
	{
		intLastMatch = FixNextItem (
			builder1 ,
			intLastMatch );
	}	// while ( intLastMatch > ListInfo.INDEXOF_NOT_FOUND )

	//  ----------------------------------------------------------------
	//  Close the array by replacing the last French brace with a square
	//	bracket.
	//  ----------------------------------------------------------------

	builder1.Replace (
		END_BLOCK_ANTE ,
		END_BLOCK_POST );

	return builder1.ToString ( );
}	// public string ApplyFixups_Pass_2

ApplyFixups_Pass_1 不同,此方法使用两个硬编码的字符串对,第一个字符串对仅应用一次,用于转换 TimeSeriesDaily,这是应用于属性的中间名称,该属性在最终 JSON 字符串中成为 Time_Series_Daily 数组。除了赋予属性最终名称外,这种一次性转换还将 TimeSeriesDaily 属性转换为数组,方法是将开头的 { 字符替换为 [ 字符。

初始化 intLastMatch 以便循环忽略字符串的开头,而无需(实际上是浪费)搜索,这需要将 StringBuilder 暂时转换为字符串,因为 StringBuilder 缺少 IndexOf 方法。通过定义一个 StringBuilder 扩展方法(无疑命名为 IndexOf)可以消除这种浪费的开销。由于此方法是为了满足一次性需求而产生的,因此该任务被搁置了,因为它将需要对 WizardWrx.Core 库进行又一次的添加。

在创建数组的一次性转换之后,控制落入主 while 循环,该循环一直执行,直到私有方法 FixNextItem 返回 ListInfo.INDEXOF_NOT_FOUND-1),这表明最后一个数组元素已被找到并修复。

实例方法 FixNextItem 通常与其调用方类似,只是替换任务通过另一个实例方法 FixNextItem 传递,并通过该方法返回。

private int FixNextItem (
 	StringBuilder pbuilder ,
	 int pintLastMatch )
{ 	// This method references private instance member _fIsFirstPass several times.
	const string FIRST_ITEM_BREAK_ANTE = "[\n \""; // Ante: },\n "
	const string SUBSEQUENT_ITEM_BREAK_ANTE = "},\n \""; // Ante: },\n "

	string strInput = pbuilder.ToString ( );
	int intMatchPosition = strInput.IndexOf (
	_fIsFirstPass
		? FIRST_ITEM_BREAK_ANTE
		: SUBSEQUENT_ITEM_BREAK_ANTE ,
		pintLastMatch );

	if ( intMatchPosition > ListInfo.INDEXOF_NOT_FOUND )
	{
		return FixThisItem (
			strInput ,
			intMatchPosition ,
			_fIsFirstPass
				? FIRST_ITEM_BREAK_ANTE.Length
				: SUBSEQUENT_ITEM_BREAK_ANTE.Length ,
			pbuilder );
	} // TRUE (At least one match remains.) block, if ( intMatchPosition > ListInfo.INDEXOF_NOT_FOUND )
	else
	{
		return ListInfo.INDEXOF_NOT_FOUND;
	} // FALSE (All matches have been found.) block, if ( intMatchPosition > ListInfo.INDEXOF_NOT_FOUND )
} // private int FixNextItem

在不深入研究 StringBuilder 实例的内部工作原理,并且可能需要编写和测试一个或多个扩展方法的情况下,我被迫吸收了将 StringBuilder(作为参数传递给 FixNextItem)转换为新字符串并将其传递给 FixThisItem 的成本。

在功能上,FixThisItemTime_Series_Daily 对象上的 Activity_Date 属性 转换为 Time_Series_Daily 数组 的属性。在返回时,它还负责关闭 _fIsFirstPass 状态标志。虽然在每次迭代中执行此操作是过度的,但我找不到其他不包装在测试中的方法;我决定每次迭代都设置它在计算上更便宜。静态方法 ArrayInfo.OrdinalFromIndex 递增其输入值,使返回值指向最后一个替换项之后的字符,这是下一个字符串扫描开始的位置。ArrayInfo 是一个静态类,由 WizardWrx.Core.dll 导出到根 WizardWrx 命名空间。

private int FixThisItem (
	string pstrInput ,
	int pintMatchPosition ,
	int pintMatchLength ,
	StringBuilder psbOut )
{
	const string FIRST_ITEM_BREAK_POST = "\n {\n \"Activity_Date\": \""; // Post: },\n {\n {\n "Activity_Date": "
	const string SUBSEQUENT_ITEM_BREAK_POST = ",\n {\n \"Activity_Date\": \""; // Post: },\n {\n {\n "Activity_Date": "

	const int DATE_TOKEN_LENGTH = 11;
	const int DATE_TOKEN_SKIP_CHARS = DATE_TOKEN_LENGTH + 3;

	int intSkipOverMatchedCharacters = pintMatchPosition + pintMatchLength;

	psbOut.Clear ( );

	psbOut.Append ( pstrInput.Substring (
		ListInfo.SUBSTR_BEGINNING ,
		ArrayInfo.OrdinalFromIndex ( pintMatchPosition ) ) );
		psbOut.Append ( _fIsFirstPass
			? FIRST_ITEM_BREAK_POST
			: SUBSEQUENT_ITEM_BREAK_POST );
	psbOut.Append ( pstrInput.Substring (
		intSkipOverMatchedCharacters ,
		DATE_TOKEN_LENGTH ) );
	psbOut.Append ( SpecialCharacters.COMMA );
	psbOut.Append ( pstrInput.Substring ( intSkipOverMatchedCharacters + DATE_TOKEN_SKIP_CHARS ) );

	int rintSearchResumePosition = pintMatchPosition
		+ ( _fIsFirstPass
			? FIRST_ITEM_BREAK_POST.Length
			: SUBSEQUENT_ITEM_BREAK_POST.Length );
	_fIsFirstPass = false; // Putting this here allows execution to be unconditional.

	return ArrayInfo.OrdinalFromIndex ( rintSearchResumePosition );
} // private int FixThisItem

最后,第二次一次性替换完成了将 Time_Series_Daily 属性转换为数组的任务,方法是用 ] 替换其结束的 }

ConsumeResponse

最后阶段 ConsumeResponse 报告 JSON 反序列化器返回的主要 TimeSeriesDailyResponse 对象上的属性。由于有很多值得关注的点,因此完整的方法如下所示。

internal static void ConsumeResponse (
	string pstrReportFileName ,
	TimeSeriesDailyResponse timeSeriesDailyResponse )
{
	Console.WriteLine (
		Properties.Resources.MSG_RESPONSE_METADATA , // Format control string
		new object [ ]
		{
			timeSeriesDailyResponse.Meta_Data.Information , 	// Format item 0: Information = {0}
			timeSeriesDailyResponse.Meta_Data.Symbol , 			// Format Item 1: Symbol = {1}
			timeSeriesDailyResponse.Meta_Data.LastRefreshed , 	// Format Item 2: LastRefreshed = {2}
			timeSeriesDailyResponse.Meta_Data.OutputSize , 		// Format Item 3: OutputSize = {3}
			timeSeriesDailyResponse.Meta_Data.TimeZone , 		// Format Item 4: TimeZone = {4}
			timeSeriesDailyResponse.Time_Series_Daily.Length , 	// Format Item 5: Detail Count = {5}
			Environment.NewLine 								// Format Item 6: Platform-dependent newline
		} );

	string strAbsoluteInputFileName = AssembleAbsoluteFileName ( pstrReportFileName );

	using ( StreamWriter swTimeSeriesDetail = new StreamWriter ( strAbsoluteInputFileName ,
		FileIOFlags.FILE_OUT_CREATE ,
		System.Text.Encoding.ASCII ,
		MagicNumbers.CAPACITY_08KB ) )
	{
		string strLabelRow = Properties.Resources.MSG_RESPONSE_DETAILS_LABELS.ReplaceEscapedTabsInStringFromResX ( );
		swTimeSeriesDetail.WriteLine ( strLabelRow );
		string strDetailRowFormatString = ReportHelpers.DetailTemplateFromLabels ( strLabelRow );

		for ( int intJ = ArrayInfo.ARRAY_FIRST_ELEMENT ;
			intJ < timeSeriesDailyResponse.Time_Series_Daily.Length ;
			intJ++ )
		{
			Time_Series_Daily daily = timeSeriesDailyResponse.Time_Series_Daily [ intJ ];
			swTimeSeriesDetail.WriteLine (
			strDetailRowFormatString ,
			new object [ ]
			{
				ArrayInfo.OrdinalFromIndex ( intJ ) , 			// Format Item 0: Item
				Beautify ( daily.Activity_Date) , 				// Format Item 1: Activity_Date
				Beautify ( daily.Open ) , 						// Format Item 2: Open
				Beautify ( daily.High ) , 						// Format Item 3: High
				Beautify ( daily.Low ) , 						// Format Item 4: Low
				Beautify ( daily.Close ) , 						// Format Item 5: Close
				Beautify ( daily.AdjustedClose ) , 				// Format Item 6: AdjustedClose
				Beautify ( daily.Volume ) , 					// Format Item 7: Volume
				Beautify ( daily.DividendAmount ) , 			// Format Item 8: DividendAmount
				Beautify ( daily.SplitCoefficient ) 			// Format Item 9: SplitCoefficient
			} );
		} // for ( int intJ = ArrayInfo.ARRAY_FIRST_ELEMENT ; intJ < timeSeriesDailyResponse.Time_Series_Daily.Length ; intJ++ )
	} // using ( StreamWriter swTimeSeriesDetail = new StreamWriter ( strAbsoluteInputFileName , FileIOFlags.FILE_OUT_CREATE , System.Text.Encoding.ASCII , MagicNumbers.CAPACITY_08KB ) )

	Console.WriteLine (
		ShowFileDetails ( 										// Print the returned string.
			Properties.Resources.FILE_LABEL_CONTENT_REPORT , 	// string pstrLabel
			strAbsoluteInputFileName , 							// string pstrFileName
			true , 												// bool pfPrefixWithNewline = false
			false ) ); 											// bool pfSuffixWithNewline = true
} // private static void ConsumeResponse
  1. 第一个打印语句唯一的显著特点是我附加的行注释,它们将参数数组中的每个项与它进入输出的格式项关联起来。这种根深蒂固的习惯通过帮助我在编写代码时捕获 WriteLine 语句的许多错误,从而节省了大量麻烦。
  2. 字符串 strLabelRow 使用 ReplaceEscapedTabsInStringFromResX(另一个扩展方法)来清理嵌入托管字符串资源中的制表符所需的双反斜杠。
  3. ReportHelpers.DetailTemplateFromLabels(由 WizardWrx.Core.dll 导出到根 WizardWrx 命名空间)生成格式控制字符串,该字符串用于将标签行后面的所有内容写入制表符分隔的文本文件中。此方法还有一个允许指定分隔符的重载。使用此方法完全消除了编写这些格式控制字符串的繁琐且容易出错的过程。
  4. 接下来的块是一个常规的 for 循环,它迭代 Time_Series_Daily 元素的数组。WriteLine 语句的布局遵循项目 1 中描述的思路。
  5. 最后,调用实用工具方法 ShowFileDetails 来在程序输出控制台上报告文件详细信息。ShowFileDetails 构建一个 FileInfo 对象,然后调用一个同名扩展方法,该方法返回一个格式化的报告,该报告被馈送到 Console.WriteLine

扩展方法 ShowFileDetailsWizardWrx.Core.dll 导出到根 WizardWrx 命名空间,该 DLL 可作为同名的 NuGet 包获得。

历史

2019 年 6 月 23 日星期日:首次发布

© . All rights reserved.