1

When filling in a form, the user needs to specify an amount. This amount is then checked against approximately 4 to 6 ranges. The selected range is then saved in the database. The original amount will not be stored (for non-technical reasons). There will be no overlay between the ranges, e.g.:

  • 0-999
  • 1000-1999
  • 2000-4999
  • 5000-9999
  • 10000-higher

The tricky part is that these ranges are not fixed in stone. There can be alterations and additional ranges can be added to further specify the '10000 and higher' range. These changes will occur a couple of times and can't be prevented. The old ranges will need to be stored since the specific amount can not be saved to the database.

What would be the most efficient C# data structure for checking against a changing set of ranges?

For my research I included:

  • One of the answers here suggest that a fixed set of integer ranges in a switch statement is possible with C#7. However, it is not possible to dynamically add cases to and/or remove cases from a switch statement.

  • This question suggests that using Enumerable.Range is not the most efficient way.

Eli-ne
  • 11
  • 3
  • Obviously switch isnt going to work for you.. what else can you do, your second question is pointing a way, store the ranges in memory and iterate through them. whether it be an array a list, or an IEnumerable.. Scanning 5 elements in anything isn't going to be a huge performance hit, unless this is mission critical to be ultra performant – TheGeneral Jan 22 '19 at 12:40
  • Take note that there will be a built-in `Range` type in C# 8 (I have no clue if it will help you) – Cid Jan 22 '19 at 12:43
  • You are mixing several Problems here. 1. How to save a "range" in DB? 2. How to handle changes to the ranges? These need to be adressed separately. – Fildor Jan 22 '19 at 12:45
  • Since you cannot save the original salaries (I guess that's what it is, right?) you lose Information, while persisting a range. Example: "2031.49" => "2k - 4.999". Now if that range is changed to "1.5k - 4.5k", the old Information will be invalid, because you have no means to tell if the orginial value would still be in the range. So you need introduce "historical" data if you want to stay consistent. – Fildor Jan 22 '19 at 12:48
  • @Fildor I am working on a way to preserve the ranges in my database. I tried to keep it to one question. The 1.000 is a thousand-separator, I'll change it in my question. – Eli-ne Jan 22 '19 at 13:00
  • I am aware of that. I am afraid answers will focus on one of those aspects. – Fildor Jan 22 '19 at 13:01
  • `most efficient` in terms of what? Performance/memory? Developability/maintainability? This seems like a such a small set of data/computation that performance differences between different solutions will probably be negligible – devNull Jan 22 '19 at 13:03

3 Answers3

1

A simple approach here is to store the lower band values in an array, and pass it to a FindBand() method which returns an integer representing the index of the band containing the value.

For example:

public static int FindBand(double value, double[] bandLowerValues)
{
    for (int i = 0; i < bandLowerValues.Length; ++i)
        if (value < bandLowerValues[i])
            return Math.Max(0, i-1);

    return bandLowerValues.Length;
}

Test code:

double[] bandLowerValues = {0, 1, 2, 5, 10};

Console.WriteLine(FindBand(-1, bandLowerValues));
Console.WriteLine(FindBand(0, bandLowerValues));
Console.WriteLine(FindBand(0.5, bandLowerValues));
Console.WriteLine(FindBand(1, bandLowerValues));
Console.WriteLine(FindBand(1.5, bandLowerValues));
Console.WriteLine(FindBand(2.5, bandLowerValues));
Console.WriteLine(FindBand(5, bandLowerValues));
Console.WriteLine(FindBand(8, bandLowerValues));
Console.WriteLine(FindBand(9.9, bandLowerValues));
Console.WriteLine(FindBand(10, bandLowerValues));
Console.WriteLine(FindBand(11, bandLowerValues));

This isn't the fastest approach if there are a LOT of bands, but if there are just a few bands this is likely to be sufficiently fast.

(If there were a lot of bands, you could use a binary search to find the appropriate band, but that would be overkill for this in my opinion.)

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • `double value` - I am pretty confident we are dealing with monetary amounts. So I would recommend not to use `double`, though it may not make a difference in this case. – Fildor Jan 22 '19 at 12:50
  • Yes, use the `decimal` type for currency, but the OP's code doesn't specify an `m` suffix for the floating point numbers, so as it stands they are `double`. – Matthew Watson Jan 22 '19 at 12:53
  • Agreed, I just like to make that recommendation because I once had _a lot_ of trouble refactoring from one to the other. So I like to save others the headaches. – Fildor Jan 22 '19 at 12:55
1

You can sort low bounds, e.g.

// or decimal instead of double if values are money
double[] lowBounds = new double[] {
      0, // 0th group:  (-Inf ..     0)
   1000, // 1st group:     [0 ..  1000)
   2000, // 2nd group:  [1000 ..  2000)
   5000, // 3d  group:  [2000 ..  5000)
  10000, // 4th group:  [5000 .. 10000)
         // 5th group: [10000 ..  +Inf)
};

and then find the correct group (0-based)

   int index = Array.BinarySearch(lowBounds, value);

   index = index < 0 ? index = -index - 1 : index + 1;

Demo:

  double[] tests = new double[] {
      -10,
        0,
       45,
      999,
     1000,
     1997,
     5123,
    10000,
    20000,
  };

  var result = tests
    .Select(value => {
      int index = Array.BinarySearch(lowBounds, value);

      index = index < 0 ? index = -index - 1 : index + 1;

      return $"{value,6} : {index}";
    });

  Console.Write(string.Join(Environment.NewLine, result));

Outcome:

   -10 : 0
     0 : 1
    45 : 1
   999 : 1
  1000 : 2
  1997 : 2
  5123 : 4
 10000 : 5
 20000 : 5
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
0

Since there are already great answers regarding how to find the correct range, I'd like to address the persistence issue.

What do we have here?

  1. You cannot persist the exact value. ( Not allowed )
  2. Values will be "blurred" by fitting them into a range.
  3. Those ranges can (and will) change over time in bounds and number.

So, what I would probably do would be to persist lower and upper bound explicitly in the db. That way, if ranges change, old data is still correct. You cannot "transform" to the new ranges, because you cannot know if it would be correct. So you need to keep the old values. Any new entries after the change will reflect the new ranges.

One could think of normalization, but honestly, I think that would be overcomplicating the problem. I'd only consider that if the benefit (less storage space) would greatly outweigh the complexity issues.

Fildor
  • 14,510
  • 4
  • 35
  • 67
  • I have decided to save the selected range together with the possible ranges in the database as a JSON string, such as `{"Ranges":[1000,2000,5000,10000], "SelectedRange":0}`. Index 0 will be converted to range 0 - 999, index 1 will result in 1000-2000 and so forth. This way, the old data will not be lost when new ranges are chosen. – Eli-ne Feb 05 '19 at 11:46