Adapting JSON Strings for Deserializing into C# Objects





5.00/5 (2 votes)
Some JSON strings need a little help fitting into a C# object.
Introduction
Two recent projects have revolved around desrializing JSON strings returned by REST API endpoints into instances of C# classes. While most deserializations involved in the first project were routine, one in that project, and all of them in the most recent project. contained text that prevented their direct use as inputs to a JSON deserializer such as Newtonsoft.Json.JsonConvert.DeserializeObject<T>
. For the first project, I devised a string extension method to apply pairs of substitutions to a string. For the more recent project, I refined it further, and added another method that performs a more complex set of string substitutions.
Background
To be suitable for deserialization into a C# object, a JSON string must look something like the following fragment.
{
"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"
}
]
}
This fragment represents a root object and two child objects.
Meta_Data
is a scalar object that has five properties, each represented by a name-value pair.Time_Series_Daily
is an array that contains an arbitrary number of scalar objects, each of which has nine properties, also represented by name-value pairs.
The JSON string returned by the REST API endpoint looks like the following fragmen.
{
"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"
}
]
}
There are several things wrong with the JSON string shown above.
- Most of the object and property names contain embedded spaces, parentheses, full stops, and other characters that are invalid in the name of a C# variable.
- The
Time Series (Daily)
elements, destined to become theTime_Series_Daily
array, are organized as a set of named properties on theTime_Series_Daily
object, each of which is a scalar object that sports eight properties. - The names of the eight properties begin with a numeral, which is invalid as the first character in the name of a C# variable.
Even if you are willing to settle for a dynamically generated object, processing that object would be cumbersome. The data set is a time series of stock market data that begs to be treated as an array of data points. With a few transformations, most of which are straightforward, the string returned by the REST endpoint can be converted into something that is easy to deserialize into an array that can be processed very efficiently.
The other knowledge that is essential to solving this puzzle is that the Paste Special feature of the Visual Studio code editor can transform well-formed XML or JSON into a class definition.
- Copy the JSON (or XML) into the Windows Clipboard.
- Use the Solution Explorer to define a new class.
- Use the Paste Special tool on the Edit menu to paste the string into the empty class, replacing the entire class definition. Leave the namespace block.
- The editor names the outermost element (class)
RootObject
. Rename it to match the name that you gave to the empty class when you created it., While you can get away without renaming the root object if you create only one class by this technique, leaving it is a stinky code smell.
Using the code
The demonstration package that accompanies this article is the GitHub repository at https://github.com/txwizard/LineBreakFixupsDemo/. Instructions for setting up and using the code are in the like-named section of Line Breaks: From Windows to Unix and Back, and I recommend that you follow them to the letter even if you intend to run through it in the Visual Studio debugger. My reason for recommending that you follow the instructions to the letter is that, although the dependencies can be acquired from the NuGet Gallery, and the project can be built from the supplied source, the test data files must be extracted from a ZIP file as directed, so that it is where the demonstration program expects to find them.
You may run the program from a command prompt, the Run box, the File Explorer, or the Visual Studio debugger, or you may use a command line argument, TransformJSONString
, to run the JSON conversion demonstration discussed in this article by itself. The argument can be appended to the command string in a command prompt window or the Run box, or it can be added to the Debug tab of the Visual Studio project property editor.
Points of Interest
The conversion happens in two passes (three, counting the line break fixups discussed in Line Breaks: From Windows to Unix and Back). Static method PerformJSONTransofmration
, shown below, and defined in Program.cs
, oversees the transformation, deserialization, and generation of a report that lists the time series data points.
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
This method is mostly straight-line code.
- Static method
Utl.GetRawJSONString
reads the JSON response returned by the REST endpoint into a string from a text file. If the input file has passed through any process, such as a Git client, that might have replaced the expected Unix line breaks with Windows line breaks, theUnixLineEndings
strRawResponse
is guaranteed to have Unix line breaks. In a production application, I would expect to assume nothing, and always callUnixLineEndings
. - A new
JSONFixupEngine
object is constructed. Its string argument,@"TIME_SERIES_DAILY_ResponseMap"
, is the name of an embedded text file resource that contains the list of string substitution pairs, about which I shall explain shortly. - Instance method
ApplyFixups_Pass_1
takesstrRawResponse
as input and returnsstrFixedUp_Pass_1
(love the imaginative names, eh?). This method wraps a call to theApplyFixups
method on private instance member_responseStringFixups
, aStringFixups
object that the constructor creates from the substitution pairs that it reads from embedded text file resourceTIME_SERIES_DAILY_ResponseMap.TXT
. - Instance method
ApplyFixups_Pass_2
takesstrFixedUp_Pass_1
as input, returningstrFixedUp_Pass_2
. - Static method
Utl.ConsumeResponse
takes string argumentpstrResponseObjectFileName
, the name to assign to the tab-delimited report file, and theTimeSeriesDailyResponse
object returned by feeding stringstrFixedUp_Pass_2
into static methodNewtonsoft.Json.JsonConvert.DeserializeObject<T>
. This method returns void.
Between each step described above, static void method Utl.PreserveResult
writes the output string that was just created into a new text file, then prints the file name and other statistics onto the console log.
ApplyFixups_Pass_1: The First Transformation
The first transformation uses the substitution string pairs shown in Table 1, which the JSONFixupEngine
constructor stores into the array of StringFixups.StringFixup
structures that it constructs from the embedded text file resource and feeds into the StringFixups
constructor. Ultimately, StringFixups
is a container for an array of StringFixup
structures that is fed by its own ApplyFixups
method to the like-named extension method on its string argument.
The StringFixups
constructor is noteworthy for its use of LoadStringFixups
, reproduced below, which, in turn, combines LoadTextFileFromEntryAssembly
, a static method on class WizardWrx.
EmbeddedTextFile
.Readers
, and a WizardWrx.AnyCSV.Parser
instance to split an embedded text file into an array of StringFixups.StringFixup
structures.
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
As insurance against data corruption, LoadStringFixups
sanity checks the label row and each detail row in the input file. If anything is amiss, an exception arises, and is expected to be caught and reported, since the accompanying message supplies significant detail that is intended to pinpoint the source of the corruption.
With respect to my decision to embed the file in the assembly, it was one fewer thing to manage. The file lives in the source code folder, belongs to the project, and is marked as Embedded Resource content. Every time the project is built, the text file is copied into the assembly, so that it is always there when the program executes.
ArrayInfo.ARRAY_FIRST_ELEMENT
and ArrayInfo.NEXT_INDEX
, both defined in WizardWrx.Common.dll
, and exported into the root WizardWrx
namespace, and several constants exported by WizardWrx.AnyCSV.dll to initialize local constants defined and used by LoadStringFixups
. These are but a few of many constants, most of which belong to the numerous static classes exported into the root WizardWrx
namespace by WizardWrx.Common.dll
, which is available as a NuGet package, WizardWrx.Common
. In addition to these constants, the managed string resources defined in WizardWrx.Common.dll
are marked as Public, so that any assembly can use them.
These constants and public strings are huge labor-savers, not to mention the improvement they bring in code readability.
Table 1 lists the string substitution pairs. The strings in the left column, labeled JSON, are the strings returned by the REST endpoint. The strings in the right column, labeled VS, are the valid variable names that replace them.
JSON | VS |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String extension method ApplyFixups
is a straightforward for
loop that iterates over the StringFixup
array that is passed into it, applying each element in turn to the input string. The first iteration initializes output string rstrFixedUp
from argument pstrIn
, which subsequent iterations take as their input, returning a new rstrFixedUp
string.
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: The Second Transformation
JSONFixupEngine
instance method ApplyFixups_Pass_2
, which performs the second phase of the string transformation, is a bit less straightforward, because the first iteration of the central while
loop that is responsible for most of its processing uses a Boolean state flag property, _fIsFirstPass, to alter its behavior on subsequent iterations. The whole method body is shown next.
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
Unlike ApplyFixups_Pass_1
, this method employs two hard coded string pairs, the first of which is applied once only to transform TimeSeriesDaily
, the intermediate name applied to the property that becomes the Time_Series_Daily
array in the final JSON string. In addition to giving the property its final name, this one-off transformation converts it into an array by replacing its opening {
character with a [
character.
Initializing intLastMatch
so that the loop ignores the beginning of the string, which it is no longer necessary (and is, indeed, wasteful) to search requires the StringBuilder
to be temporarily transformed into a string, because StringBuilder
lacks an IndexOf
method. This wasted motion could be eliminated by defining a StringBuilder
extension method (named IndexOf
, no doubt). Since this method came into being to meet a one-off requirement, that task was set aside, as it would have required yet another addition to the WizardWrx.Core
library.
Following the one-off transformation that creates the array, control falls into the main while
loop, which executes until private method FixNextItem
returns ListInfo.INDEXOF_NOT_FOUND
(-1), indicating that the last array element has been found and fixed.
Instance method FixNextItem
is generally like its caller, except that the replacement task is handed off to another instance method, FixNextItem
, through which it returns.
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
Without digging more deeply into the workings of StringBuilder
instances, and probably writing and testing one or more extension methods, I was forced to absorb the cost of converting the StringBuilder
that is fed into FixNextItem
into a new string and passing it into FixThisItem
.
Functionally, FixThisItem
transforms the Activity_Date
property on the Time_Series_Daily
object into a property of the Time_Series_Daily
array. As it returns, it is also responsible for turning off the _fIsFirstPass
state flag. While doing this on every iteration is overkill, I couldn’t find another way that wouldn’t require it to be wrapped in a test; I decided it was computationally cheaper to set it on every iteration. Static method ArrayInfo.OrdinalFromIndex
increments its input value, causing the return value to point to the character that immediately follows the last substitution, which is where the next string scan begins. ArrayInfo
is a static class exported into the root WizardWrx
namespace by WizardWrx.Core.dll
.
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
Finally, a second one-off replacement completes the task of converting the Time_Series_Daily
property into an array by exchanging its closing }
for a ]
.
ConsumeResponse
The final phase, ConsumeResponse
, reports the properties on the main TimeSeriesDailyResponse
object returned by the JSON deserializer. Since there are many points of interest, the whole method is reproduced next.
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
- The only remarkable feature of the first print statement is the line comments that I appended to associate each item in the parameter array with the format item through which it makes its way into the output. This ingrained habit has saved much grief by helping me catch many errors in the construction of WriteLine statements as the code is written.
- String
strLabelRow
usesReplaceEscapedTabsInStringFromResX
, another extension method, to clean up the double backslashes that are required to embed tabs in a managed string resource. ReportHelpers.DetailTemplateFromLabels
, exported into the rootWizardWrx
namespace byWizardWrx.Core.dll
, generates the format control string required to write everything in the tab delimited strings that follow a label row into a tab delimited text file. This method also has an overload that allows the delimiter to be specified. Using this method eliminates entirely the tedious, error-prone process of writing these format control strings.- The next block is a conventional
for
loop that iterates through the array ofTime_Series_Daily
elements. TheWriteLine
statement is laid out along the lines described in item 1. - Finally, utility method
ShowFileDetails
is called upon to report the file details on the program output console.ShowFileDetails
constructs aFileInfo
object, then invokes a like-named extension method that returns a formatted report, which is fed intoConsole.WriteLine
.
Extension method ShowFileDetails
is exported into the root WizardWrx
namespace by WizardWrx.Core.dll
, which is available as a like-named NuGet package.
History
Sunday, 23 June 2019: Initial Publication