5
\$\begingroup\$

I'm a junior developer currently working on a C# project. There is a lot more for me to learn so here I am looking for some advice on my code snippet.

I have a function that executes a number of processes and steps. I'm looking to log the execution of each step in a single string and insert that string into my database for daily review purposes. The code snippet below is able to accomplish this but looking at it, personally it is an eyesore and I'm sure there has to be a more efficient way to accomplish this but nothing comes to mind due to my lack of in depth knowledge. I was wondering if anybody could suggest a better alternative to forming the final "result" string.

Note: I removed the processes and functions involved in the original snippet to focus on the area I am looking to improve. The focus is purely on forming the "result" string. The output is as I expect it to be but I'm merely looking for a more efficient way to achieve it, alongside possibly learning something new. Any and all advice is appreciated.

Here is a sample of the possible final output: "Balloting IPO(s): 1005, 1006, 1007. Processed Tape for Share Issue Number(s): 1005 (Tape02), 1006 (Tape01). No Tape found for Share Issue Number(s): 1006."

if (ballotingIPOs.Count > 0)
{
    string sBallotIPO = "Balloting IPO(s): ";
    string sProcessTape = "Processed Tape for Share Issue Number(s): ";
    string sNoTape = "No Tape found for Share Issue Number(s): ";
    string tsBallotIPO = string.Empty;
    string tsProcessTape = string.Empty;
    string tsNoTape = string.Empty;

    foreach (IPODetails ballotingIPO in ballotingIPOs)
    {
        tsBallotIPO += tsBallotIPO.Length > 0 ? $", {ballotingIPO.ShareIssueNumber}" : $"{ballotingIPO.ShareIssueNumber}";

        if (ipoData.ipoDetails.ShareIssueNumber != null)
        {
            tsProcessTape += tsProcessTape.Length > 0 ? $", {ipoData.ipoDetails.ShareIssueNumber} (Tape{ipoData.ipoDetails.TapeNumber})" : $"{ipoData.ipoDetails.ShareIssueNumber} (Tape{ipoData.ipoDetails.TapeNumber})";
        }
        else
        {
            tsNoTape += tsNoTape.Length > 0 ? $", {ballotingIPO.ShareIssueNumber}" : $"{ballotingIPO.ShareIssueNumber}";
        }
    }

    result += tsNoTape.Length > 0 ? $"{sBallotIPO}{tsBallotIPO}. {sProcessTape}{tsProcessTape}. {sNoTape}{tsNoTape}." : $"{sBallotIPO}{tsBallotIPO}. {sProcessTape}{tsProcessTape}.";
}
else
{
    result += "No Balloting IPOs to process.";
}
\$\endgroup\$
9
  • \$\begingroup\$ Use StringBuilder rather than string. Also don't use $"{ballotingIPO.ShareIssueNumber}" - use ballotingIPO.ShareIssueNumber instead. \$\endgroup\$ Commented Jan 9 at 3:39
  • \$\begingroup\$ Please share code that compiles. e.g. result is undeclared. \$\endgroup\$ Commented Jan 9 at 3:39
  • \$\begingroup\$ Don't need to ever test for .length > 0 if you append (vice prepend) the comma, i.e. ballotingIPO.ShareIssueNumber + ", ". What does it matter that there will be a "dangling" comma at the very end. \$\endgroup\$ Commented Jan 9 at 6:01
  • \$\begingroup\$ get rid of the else by testing terminating conditions up front. if (ballotingIPOs.Count <= 0) return; \$\endgroup\$ Commented Jan 9 at 6:05
  • 1
    \$\begingroup\$ Could you please just show us a sample of the final output of this part of the code ? (and a short explanation to relate the code would be appreciated). \$\endgroup\$ Commented Jan 9 at 12:32

3 Answers 3

7
\$\begingroup\$

In general,

s += s.Length > 0 ? ", someLongText" : "someLongText";

can be simplified to

s += (s.Length > 0 ? ", " : "") + "someLongText";

which already removes some duplicate code.

However, for comma-separated output, using lists instead of string concatenation allows you to utilize the built-in string.Join method. So, instead of:

string s = "";
for (...)
{
    s += (s.Length > 0 ? ", " : "") + "someLongText";
}

you can write:

var messages = new List<string>();
for (...)
{
    messages.Add("someLongText");
}
string s = string.Join(", ", messages);
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Definitely looks cleaner, I'll implement both versions and see which comes out better but pretty sure it will be the string.Join method. Thank you for that \$\endgroup\$ Commented Jan 10 at 0:57
  • \$\begingroup\$ I would further add, string concatenation in a loop is generally a code smell. It is one of those things that seems simple and will work nicely at your desk but can blow up in the real world because it makes the loop O(N^2) as the system has to keep reallocating and copying to progressively larger strings. If you have to build strings up in a loop use StringBuilder, although in the specific case of merging a list of strings with a seperator, Heinzi's suggestion of string.Join is the right one. \$\endgroup\$ Commented Jan 24 at 17:50
3
\$\begingroup\$

the issue here is not how to make it readable, I'm just concerned about the overall string memory allocation for this application. As it seems there are a lot of similar string concatenation else where, which is something you need to worry about. There are better methods to reduce string memory allocation and also improve code readability, but again, depends on your current project status and how much efforts you will put on it, and is it going to be worth that efforts? or is there any plan to upgrade or change the application ?! ...etc. many questions and thoughts around changes like that. So, you decide which is better.

For this code and any future code, I would like to share one way to hold this kind of issues.

Whenever you need to build an output to the consumer as string, it would be best if you first build a class model for that output, to hold the object values, which can be reused. Then, use that model to represent the output in any presentation format you like such as normal text, csv, JSON, pdf, html ..etc. All can use the same object to reproduce the same exact result in different format. It's also maintainable, and would give you the ability to move the presentation logic into its layer.

Here is an example :

public sealed class IPOTape
{
    private readonly int _ipo;
    private readonly string _tapeNumber;
    
    public int IPO => _ipo;
    
    public string TapeNumber => _tapeNumber;
    
    public IPOTape(int ipo, string tapeNumber)
    {
        _ipo = ipo;
        _tapeNumber = tapeNumber;
    }
    
    public override string ToString()
    {
        return string.Format("{0} ({1})", _ipo, _tapeNumber);
    }
}

public sealed class BallotingIPOResult
{
    public List<int> IPOs { get; private set; } 
    
    public List<IPOTape> Tapes { get; private set;  } 
    
    public List<int> NoTapes { get; private set;  }  
    
    private BallotingIPOResult() { }
    
    
    public static BallotingIPOResult Create(Dictionary<int, List<IPOTape>> dictionary)
    {
        // do the necessary checks here
        
        var ipos = dictionary.Select(s=> s.Key).ToList(); 

        var withTape = dictionary
            .Where(s=> s.Value.Count > 0)
            .SelectMany(s=> s.Value).ToList(); 

        var noTapes = dictionary.Where(s=> s.Value.Count == 0).Select(s=> s.Key)?.ToList() ?? new List<int>(0);
        
        return new BallotingIPOResult 
        {
            IPOs  = ipos,
            Tapes = withTape,
            NoTapes = noTapes
        }
    }
    
    public override string ToString()
    {
        // do the proper checks before 
        
        var builder = StringBuilder();  
        builder.Append(FormatString("Balloting IPO(s): {0}.", IPOs)); 
        builder.Append(" ");
        builder.AppendLine(FormatString("Processed Tape for Share Issue Number(s): {0}.", Tapes)); 
        
        if(NoTapes.Count > 0)
        {
            builder.Append(" ");
            builder.AppendLine(FormatString("No Tape found for Share Issue Number(s): {0}.", NoTapes));                     
        }
        
        return builder.ToString();
    }
    
    private static string FormatString<T>(string template, IEnumerable<T> items)
    {
        var value = !items.Any() ? string.Empty : string.Join(",", items);
        return string.Format(template, value);
    }

}

Now your code can be something like this :

if (ballotingIPOs.Count > 0)
{
    var ipoShareIssueNumber = ipoData.ipoDetails.ShareIssueNumber;

    var dictionary = new Dictionary<int, List<IPOTape>>();

    foreach (IPODetails ballotingIPO in ballotingIPOs)
    {
        var shareIssueNumber = ballotingIPO.ShareIssueNumber; 
        
        List<IPOTape> tapes = null;
        
        if(!dictionary.TryGetValue(shareIssueNumber, out var tapes))
        {
            tapes = new List<IPOTape>();
            dictionary.Add(shareIssueNumber, tapes);
        }
        
        if(ipoShareIssueNumber != null)
        {
            var tapeNumber = ipoData.ipoDetails.TapeNumber;     
            tapes.Add(new IPOTape(ipoShareIssueNumber, tapeNumber);
        }   
    }

    var resultModel = BallotingIPOResult.Create(dictionary);

    result += resultModel.ToString();

}
else
{
    result += "No Balloting IPOs to process.";
}
\$\endgroup\$
2
\$\begingroup\$
// This feels like it should be its own method
// Incorporate @mjwills comments
public string IpoLog {
   var result = new StringBuilder();
  //make this a condition-string
  var noIPOs = "No Balloting IPOs to process."

  // return immediately on terminating conditions
  if (ballotingIPOs.Count <= 0) return noIPOs;

  foreach (IPODetails ballotingIPO in ballotingIPOs) 
  { 
    ...

    // Don't bother to test `length`, just append a comma every time. No big deal. 
    tsBallotIPO += $"{ballotingIPO.ShareIssueNumber}, ";
  }// forEach

  // trim all substrings as needed after building them
  toBalloIPO.TrimEnd(","); 
 
  . . . 
  
  return result.ToString().TrimEnd(",");
} // IpoLog
\$\endgroup\$
9
  • \$\begingroup\$ I did initially just append a comma every time. The code was much more bearable then but let's just say the client is unhappy with that and wants it corrected. Hence, the fairly unnecessary snippet. But your first three comments are valid along with @mjwills comments so I will implement that. \$\endgroup\$ Commented Jan 9 at 6:57
  • \$\begingroup\$ Remove the trailing comma after the log-string is complete, just before returning. Thousands of unnecessary if executions is a good example of "unbearable." See MSDN C# String.TrimEnd() \$\endgroup\$ Commented Jan 9 at 17:28
  • \$\begingroup\$ How would that be any different than just doing a ternary? Appending a comma every time then removing the extra comma, from my POV at least (inexperienced eyes), is kind of just splitting each current ternary into two lines \$\endgroup\$ Commented Jan 10 at 0:55
  • \$\begingroup\$ Perhaps I'm misunderstanding? The ternary is cleaner, shorter, more readable than the original; in fact that was my first thought for improvement. However the conditional is still there and after the first loop is uselessly executing. By not checking length at all there will be a leading (or trailing in my answer) comma in the final assembled string. This is easily removed as shown in my answer. I mean this to apply to all the substring concatenations also. \$\endgroup\$ Commented Jan 10 at 2:30
  • \$\begingroup\$ That is true but using the method in your answer will only remove the final comma. Any trailing commas within the string will remain. Example output below using your method in my original code: "Balloting IPO(s): 1005, 1006, 1007,. Processed Tape for Share Issue Number(s): 1005 (Tape02), 1006 (Tape01),. No Tape found for Share Issue Number(s): 1006." I will end up with trailing commas in the middle of the string if I only Trim the final assembled string. I understand your point but your answer only considers having trailing commas at the end and not in the middle \$\endgroup\$ Commented Jan 10 at 2:39

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.