1

I wanted to know if there's a way to declare a lot of case statements without having to write all of them. For example, my code:

            switch (Weight)
            {
                case 0 to 2;
                    ShippingCost = "3.69";

                case 3 to 4;
                    ShippingCost = "4.86";

                case 5 to 6;
                    ShippingCost = "5.63";

                case 7 to 8;
                    ShippingCost = "5.98";

                case 9 to 10;
                    ShippingCost = "6.28";

                case 11 to 30;
                    ShippingCost = "15.72";

            }

I started converting VB to C# and realized that in order to have more than one case statements you have to declare them. As you can see I have 11 to 30 and do not wanna have all those lines.

dan byrd
  • 51
  • 3
  • 5
    No, you cannot define a "range" of values in a switch statement. For this you're want a bunch of `if-else if` statements like `if (weight >= 0 && weight <= 2) ... else if (weight >= 3 && weight <= 4)` – sab669 Nov 18 '15 at 14:40
  • If you want your "11 to 30" to be the "catch all", you can just set it as `default`. What happens if your `Weight` is over 30? Shipping is free? – Ron Beyer Nov 18 '15 at 14:42
  • Is weight an integer? If it's not a whole number then using if else statements are probably your best bet – Lloyd Powell Nov 18 '15 at 14:43
  • 2
    VB.NET For The Win on this one ;-) – Matt Wilko Nov 18 '15 at 14:43
  • Off topic: "ShippingCost" sounds like something you want to calculate with. Why then use string instead of decimal? – Hans Kesting Nov 18 '15 at 14:43
  • I agree with @sab669's comment more than the 2 current answers. With 31 total cases (including the 0 case) it'll probably look a bit cleaner. – Broots Waymb Nov 18 '15 at 14:43
  • As @sab669 astutely stated, you want if/else. 'Select Case' in VB is a very different animal than 'switch' - don't try to replicate one with the other - they are only equivalent for the simplest cases. – Dave Doknjas Nov 18 '15 at 14:44
  • Your shipping cost should also be a decimal value rather than a string – Matt Wilko Nov 18 '15 at 14:44
  • 1
    @sab669: (Off the top of my head) most of the difference is that VB allows non-ordinal 'Select Case' expressions, and non-ordinal, range-type, or non-constant 'Case' expressions. – Dave Doknjas Nov 18 '15 at 14:49
  • @DaveDoknjas Thanks! I actually didn't realize that VB does in fact support a range of values unlike C#. I've only used it very little. I thought this question was more of a "This is what I want to do it, is it possible in either language?" sort of thing. – sab669 Nov 18 '15 at 14:50
  • One day we will code all in one language called V# which will by the best of both languages and allow Case statements with ranges. I wouldn't like the `{` followed by `End Select` syntax though... – Matt Wilko Nov 18 '15 at 15:00
  • @MattWilko haha funny answer kkk. Try using f#... is so good. – Igor Quirino Nov 18 '15 at 15:23
  • This question has been answered so many times before! One interesting example how to solve it in c# is [here](http://akshaya-m.blogspot.de/2015/03/elegant-way-to-switch-if-else.html). – Daniel Dušek Nov 18 '15 at 15:59

10 Answers10

13

You cannot use comparisons in C# as you can in VB. You can however use fall-through cases, like so:

case 0:
case 1:
case 2:
  ShippingCost = "3.69";
  break;

case 3:
case 4:
  ShippingCost = "4.86";
  break;

Note that non-empty cases require either a throw, return or,break statement. Also note that you can only fall-through on empty cases.

Edit:

For completeness, as others have pointed out, it is probably more sensible in this case to use a series of if statements, like so:

if(Weight<=2) {
  ShippingCost = "3.69";
}
else if(Weight <= 4) {
  ShippingCost = "4.86";
}
... etc
Glorin Oakenfoot
  • 2,455
  • 1
  • 17
  • 19
  • 1
    Unless weight isn't a whole number – Lloyd Powell Nov 18 '15 at 14:43
  • That is true. If you are dealing with `decimal`s, `float`s, or `double`s, then you would have to go with `if` statements. – Glorin Oakenfoot Nov 18 '15 at 14:45
  • 2
    Why try to replicate VB's 'Select Case' with C#'s 'switch'? VB's 'Select Case' is a more general tool than 'switch' is intended to be - if/else is the C# equivalent to a VB 'Select Case' with ranges. – Dave Doknjas Nov 18 '15 at 14:46
  • I think it's more sensible, and short, to use a linq query on an array, as I have outlined below. – Klaus Byskov Pedersen Nov 18 '15 at 15:02
  • 1
    @KlausByskovPedersen if your query will not find an element,it will throw InvalidOperationException – Suren Srapyan Nov 18 '15 at 15:03
  • @DaveDoknjas In this case, I would agree, although I think it depends highly on context. Just because you use ranges in VB does not mean it's inappropriate to replicate in a switch in C#. Edited with an example `if`, for completeness. – Glorin Oakenfoot Nov 18 '15 at 15:04
  • @KlausByskovPedersen I like Linq, I really do, but short code length and readability are not always the same thing. I think a Linq statement takes more *interpretation* than a simple set of `if`s or a `switch`. – Glorin Oakenfoot Nov 18 '15 at 15:06
  • @SurenSrapyan, it will not throw a NRE but an error pertaining to Sequence Contains No Elements. That's why I wrote that he needs better handling for edge cases. That problem is no different in any of the other given solutions, which leave `ShippingCost` at its default value or uninitialized if none of the `if`'s evaluate to true. Same with the switch without a `default` case. – Klaus Byskov Pedersen Nov 18 '15 at 15:07
  • @GlorinOakenfoot I find that if you use Linq statements a lot they are extremely easy to interpret. But if you never use them they will remain hard to interpret. – Klaus Byskov Pedersen Nov 18 '15 at 15:10
  • @KlausByskovPedersen I use Linq very frequently, and interpreting your statement is not *hard*, but it still takes a second to think about the fact you're basically referencing a lookup table. I just personally think using Linq in this case may be a little *too* clever for such a simple situation. Either way is perfectly viable though. It just boils down to personal preference. – Glorin Oakenfoot Nov 18 '15 at 15:18
  • @KlausByskovPedersen Sounds like Atwood's Law but for LINQ. It's great, but doesn't mean you should shoehorn it into every single opportunity where it can achieve the desired results! – sab669 Nov 18 '15 at 15:31
6

There is direct equivalent in C#, however, you can use fall-through so you don't have to repeat the implementation:

switch (Weight)
{
    case 0:
    case 1:
    case 2:
       ShippingCost = "3.69";
       break;
       ...

If statements would probably suit you much better in this scenario:

if(Weight >= 0 && Weight <= 2){
    ShippingCost = "3.69";
} else if(Weight >= 3 && Weight <= 4){
    ShippingCost = "4.86";
}
...
Domysee
  • 12,718
  • 10
  • 53
  • 84
  • Tip: Switch will be compiled as "if" statemant . I use switch only for enum . – Igor Quirino Nov 18 '15 at 15:19
  • @IgorQuirino That is not always true. I'm not exactly sure of the semantics, but `switch` statements *can* definitely be interpreted as a lookup table, and not a series of conditionals. Performance is not at issue though. Correctness and readability are all that matters in such a simple piece of code. – Glorin Oakenfoot Nov 18 '15 at 15:24
5

Try like this: This solutions removes the need to write && in your else if statements

if(Weight >= 11 && Weight <= 30)
{
   ShippingCost = "15.72";
}
else if(Weight >= 9)
{
   ShippingCost = "6.28";
}
else if(Weight >= 7)
{
   ShippingCost = "5.98";
}
else if(Weight >= 5)
{
   ShippingCost = "5.63";
}
else if(Weight >= 3)
{
   ShippingCost = "4.86";
}
else if(Weight >= 0)
{
   ShippingCost = "3.69";
}
Suren Srapyan
  • 66,568
  • 14
  • 114
  • 112
  • I like this, probably the most readable of all the suggestions. Arguably not necessarily the most efficient, if you know the most likely case will be a particular range then that should be the first one to reduce the number of evaluations performed, but that doesn't really matter in most cases unless you have some *serious* real-time requirements. – sab669 Nov 18 '15 at 14:57
  • You are right.It no matter what suggestion to use if you have some experiment.But I like this approach – Suren Srapyan Nov 18 '15 at 15:00
  • @sab669 I disagree. A linq query on an array is both more compact and more easy to read, IMO. – Klaus Byskov Pedersen Nov 18 '15 at 15:04
  • 1
    @KlausByskovPedersen if your query will not find an element,it will throw InvalidOperationException – Suren Srapyan Nov 18 '15 at 15:04
  • @KlausByskovPedersen Ah, I didn't see your answer when I posted that comment.. However, based on Suren's comment.... Perhaps you should use `FirstOrDefault` and then handle that – sab669 Nov 18 '15 at 15:08
5

You could also write it as a Linq one liner:

var ShippingCost = (new[] { new { w = 2, p = "3,69" }, 
                            new { w = 4, p = "4.86" }, 
                            new { w = 6, p = "5.63" }, 
                            new { w = 8, p = "5.98" }, 
                            new { w = 10, p = "6.28" }, 
                            new { w = 30, p = "15.72" }})
             .First(x => Weight <= x.w).p;

You would, as others have already stated, want to make sure that shipping for items weighing more than 30 is also handled correctly.

Klaus Byskov Pedersen
  • 117,245
  • 29
  • 183
  • 222
  • I'll give you a +1 for thinking outside the box, though I would move that array to be `static readonly` most likely, instead of creating it on the fly. Would make the actual lookup less ugly. Though that's personal preference. – Glorin Oakenfoot Nov 18 '15 at 15:08
4

How bout just using this sort of approach

private static double GetShippingCost(double weight)
{
    if (weight > 30) throw new ArgumentException("Weight over allowed maximum", "weight");

    if (weight <= 2) return 3.69;
    if (weight <= 4) return 4.86;
    if (weight <= 6) return 5.63;
    if (weight <= 8) return 5.98;
    if (weight <= 10) return 6.28;
    if (weight <= 30) return 15.72;

}
Janne Matikainen
  • 5,061
  • 15
  • 21
3

An alternative to using a case would be to write some kind of class to do the mapping, for example:

public sealed class CostsPerWeight
{
    class CostPerWeight
    {
        public int Low;
        public int High;
        public double Cost;
    }

    readonly List<CostPerWeight> costs = new List<CostPerWeight>();

    public CostsPerWeight Add(int low, int high, double result)
    {
        // Error handling omitted for brevity. 
        // Real code should check that low < high and that ranges do not overlap.

        costs.Add(new CostPerWeight { Low = low, High = high, Cost = result } );
        return this;
    }

    public double Cost(int weight)
    {
        // This throws if the weight isn't in the list.
        // If that's not what you want, you'd have to add extra error handling here.
        return costs.First(x => x.Low <= weight && weight <= x.High).Cost;
    }
}

Which you would use like this (I've used doubles instead of strings for the costs for this example, but you can use whatever type you need):

var costs = new CostsPerWeight()
    .Add( 0,  2,  3.69)
    .Add( 3,  4,  4.86)
    .Add( 5,  6,  5.63)
    .Add( 7,  8,  5.98)
    .Add( 9, 10,  6.28)
    .Add(11, 30, 15.72);

double shippingCost = costs.Cost(weight);

If you have a lot of these switch statements in VB, it would be worth considering this approach.

(The advantage of using this instead of a Linq one-liner is simply that it's easier to document and unit test. You could also create a CostsPerWeight class instance and pass it around - useful for decoupling code, dependency-injection and for unit testing.)

It does seem to me that the concept of looking up a cost based on a weight is crying out to be encapsulated in a class, rather than embedded piecemeal in various parts of the code.

Here's a more extended example of CostsPerWeight with more error handling:

public class CostsPerWeight
{
    class CostPerWeight
    {
        public int Low;
        public int High;
        public double Cost;
    }

    readonly List<CostPerWeight> costs = new List<CostPerWeight>();

    double min = double.MaxValue;
    double max = double.MinValue;
    double costForMin;

    public CostsPerWeight Add(int low, int high, double cost)
    {
        if (low > high)
            throw new ArgumentException(nameof(low) + " must be less than " + nameof(high));

        if (cost < 0)
            throw new ArgumentOutOfRangeException(nameof(cost), "cost must be greater than zero");

        costs.Add(new CostPerWeight { Low = low, High = high, Cost = cost } );

        if (low < min)
        {
            min = low;
            costForMin = cost;
        }

        if (high > max)
            max = high;

        return this;
    }

    public double Cost(int weight)
    {
        if (weight < min)
            return costForMin;

        if (weight > max)
            throw new InvalidOperationException($"Weight {weight} is out of range: Must be <= {max}");

        return costs.First(x => x.Low <= weight && weight <= x.High).Cost;
    }
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Not sure your Linq one-liner has an advantage, because it crashes. However I love the rest if your answer, so upvote. –  Nov 18 '15 at 15:25
  • @buffjape It would crash if the values are out of range. The OP didn't define what should happen if the values are out of range, so it's up to them how to handle them. Throwing an exception on out-of-range values is a reasonable response, but possibly you'd want to throw a specific exception type. – Matthew Watson Nov 18 '15 at 15:46
  • I thought exceptions were reserved for unanticipated events (programming or filesystem errors). Out-of-range weight can be anticipated - the VB code would have used the existing value of ShippingCost. –  Nov 18 '15 at 16:03
  • @buffjape Indeed, and allowing a weight greater than the largest allowed to get as far as the lookup is probably a programming error if it happens at that level. The question that arises is what should be done if the weight is too high. I don't think the code at that level can necessarily make that decision - but this is one of those areas which is a matter of debate. :) – Matthew Watson Nov 18 '15 at 16:06
  • I need to go on a diet. –  Nov 18 '15 at 16:07
3

The clearest way is to put your data into objects instead.

  private struct ShippingCost
  {
      public int MinWeight;
      public int MaxWeight;
      public decimal Cost;

      public ShippingCost(int min, int max, decimal cost)
      {
         MinWeight = min;
         MaxWeight = max;
         Cost = cost;
      }
  }

  private List<ShippingCost> Costs = new List<ShippingCost>
  {
      new ShippingCost(0, 2, 3.69m),
      new ShippingCost(3, 4, 4.86m),
      new ShippingCost(5, 6, 5.63m),
      new ShippingCost(7, 8, 5.98m),
      new ShippingCost(9, 10, 6.28m),
      new ShippingCost(11, 30, 15.72m),
  };

  // Choose shipping cost
  public decimal CalcShippingCost(int weight)
  {
      foreach (ShippingCost sc in Costs)
      {
          if (weight >= sc.MinWeight && weight <= sc.MaxWeight)
              return sc.Cost;
      }

      return 0.00m;     // default cost
  }
1

You can't do that in C#. The best option if your max value for weight is 30 is to the use the default case.

Otherwise, if you don't want something like

case 11:
case 12:
case 13:
....
case 28:
case 29:
case 30:

an "oldschool" if/else if will be the the most readable solution

SebVb
  • 187
  • 1
  • 3
  • 14
  • Sometimes "old school" is best ;) but really they're just two different tools with very similar functionality. – sab669 Nov 18 '15 at 14:48
0

I'd recommend storing the data in a container and iterating through the container. You could create your own class or use a .net class like Tuple:

var shippingCostsByWeight = new List<Tuple<int, int, string>>
{
    new Tuple<int, int, string>(0, 2, "3.69"),
    new Tuple<int, int, string>(3, 4, "4.86"),
    new Tuple<int, int, string>(5, 6, "5.63"),
    new Tuple<int, int, string>(7, 8, "5.98"),
    new Tuple<int, int, string>(9, 10, "6.28"),
    new Tuple<int, int, string>(11, 30, "15.72"),
};

ShippingCost = shippingCostsByWeight
                .First(tuple => weight >= tuple.Item1 && weight <= tuple.Item2).Item3;
Kjartan
  • 18,591
  • 15
  • 71
  • 96
LVBen
  • 2,041
  • 13
  • 27
-1

You could use a fall-through case switch statement, which goes as the following:

case 0:
case 1:
case 2:
  shippingCost = "3.69";
  break;

... and so on

Which will cause 0 to set shippingCost to 3.69 aswell as 1 and 2. :)
That would be my Solution to this

Colin Loos
  • 21
  • 3