Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to map ranges to values?

Tags:

c++

c++11

I'm actually wondering what is a best way to assign letter to a range properly in C++.

For example, we have that scale:

enter image description here

We can do assignment in simplest way:

if(a < 40)
  return 'T';
else if(a < 55)
  return 'D';
else if(a < 70)
  return 'P';
else if(a < 80)
  return 'A';
else if(a < 90)
  return 'E';
else if(a <= 100)
  return 'O';

However, do you have some better ideas to make this?

And what when we have bigger numbers and more letters (I think that if statements can be still annoying...)? Or what if there are free spaces between ranges, e.g. 30-40 45-55 60-70?

like image 734
BartekPL Avatar asked Dec 14 '22 00:12

BartekPL


2 Answers

You can use simple arrays for sorted ranges and outputs

char getGrade (int grade) {
    int upper[] = { 40, 55, 70, 80, 90, 100 };
    int lower[] = { 0, 40, 55, 70, 80, 90 };
    char grades[] = { 'T', 'D', 'P', 'A', 'E', 'O' };

    for (int i = 0; i< 6; i++) 
        if ((grade< upper[i]) && (grade >= lower[i]))               
            return grades[i];
    return 'X'; // no grade assigned
}

Edit: I'm adding interesting implementation with struct and std::find_if suggested by @YSC

#include <iostream>
#include <algorithm>
#include <vector>

struct Data{ int lower; int upper; char grade; };

char getGrade (int grade, std::vector<Data> data) {
    auto it = std::find_if(
            data.begin(),
            data.end(), 
            [&grade](Data d) { return (d.lower<= grade) && (d.upper > grade); }
           );
    if (it == data.end())
        return 'X'; // not found
    else
        return it->grade;
}

int main () {
    const std::vector<Data> myData = { { 0, 40, 'T'} , { 40, 55, 'D'}, {55, 70, 'P'}, {70, 80, 'A'}, {80, 90, 'E'}, {90, 101, 'O'} };
    std::cout << getGrade(20, myData) << std::endl;
    return 0;
}
like image 174
Robert Eckhaus Avatar answered Dec 27 '22 13:12

Robert Eckhaus


This is a C++14 answer. Everything can be translated to C++11, just less pretty.

template<class F, class Base=std::less<>>
auto order_by( F&& f, Base&& b={} ) {
  return
    [f=std::forward<F>(f), b = std::forward<Base>(b)]
    (auto const& lhs, auto const& rhs)
    ->bool
    {
      return b( f(lhs), f(rhs) );
    };
}

order_by takes a projection and optionally a comparison function object, and returns a comparison function object that applies the projection then either std::less<> or the comparison function object.

This is useful when sorting or searching, as C++ algorithms require comparison function objects, while projections are easy to write.

template<class A, class B>
struct binary_overload_t:A,B{
  using A::operator();
  using B::operator();
  binary_overload_t( A a, B b ):A(std::move(a)), B(std::move(b)) {}
};
template<class A, class B>
binary_overload_t< A, B >
binary_overload( A a, B b ) {
  return { std::move(a), std::move(b) };
}

binary_overload lets you overload function objects.

template<class T>
struct valid_range_t {
  T start, finish;
};

This represents a valid range. I could just use std::pair, but I prefer types with meaning.

template<class T, class V>
struct ranged_value_t {
  valid_range_t<T> range;
  V value;
};
template<class T, class It>
auto find_value( It begin, It end, T const& target )
-> decltype(std::addressof( begin->value ))
{
  // project target into target
  // and a ranged value onto the lower end of the range
  auto projection = binary_overload(
    []( auto const& ranged )->T const& {
      return ranged.range.finish;
    },
    []( T const& t )->T const& {
      return t;
    }
  );
  // 
  auto it = std::upper_bound( begin, end,
    target,
    order_by( projection )
  );
  if (it == end) return nullptr;
  if (target < it->range.start) return nullptr;
  return std::addressof( it->value );
}

Now find_value takes a pair of iterators to ranged_value_t type structures arranged with non-overlapping ranges.

It then return a pointer to the entry of the first (and hence only) value whose (half open) range contains target.

ranged_value_t<int, char> table[]={
  {{0,40}, 'T'},
  {{41,55}, 'D'},
  {{56,70}, 'P'},
  {{71,80}, 'A'},
  {{81,90}, 'E'},
  {{91,101}, 'O'}
};

auto* ptr = find_value( std::begin(table), std::end(table), 83 );
if (ptr) std::cout << *ptr << "\n"; else std::cout << "nullptr\n";

Live example.

The advantages of this answer over alternatives:

  • We don't create needless value objects. If the value objects are expensive, large, or non-trivially constructible, this matters.
  • The syntax to create a table is simple
  • Tables can be in nearly any format. You can parse them from a file; the find_value function just takes iterators (and prefers them to be random access).
  • We could augment with permitting the lower bound to be omitted. We'd have to add a flag to valid_range_t (or use an optional) and consume it in find_value when we check s, and add constructors to valid_range_t to make it easy to use.

Augmenting it to support both half-open and closed intervals would take a bit of work. I'd be tempted to hack it into find_value as a second check.

Overlapping intervals also takes a bit of work. I'd do a lower_bound on start (s) and an upper_bound on finish (f).

I find this kind of stuff is best suited by data-driven design; hardcoding this in C++ code is a bad plan. Instead, you consume configuration, and you write your code to validate and be driven by that configuration.

like image 21
Yakk - Adam Nevraumont Avatar answered Dec 27 '22 12:12

Yakk - Adam Nevraumont