I think it's not correct to switch to long to go around int limits.
What if you want to do the same with long inputs?
You can do that by using double internally or by transforming the delta-calculating formulas. Forget about using double internally since it can't represent the range of long and ulong. So Closer.CheckPrint and Closer.Check won't work with huge long and ulong values. Thereby Closer.I and Closer.N are working solutions. Closer.CheckI and Closer.CheckN could be written using using BitsMinMaxI and BitsMinMaxN respectively all using long and ulong inputs and internal variables respectively. BitsMinMaxI and BitsMinMaxN have to be written with bit shift (<<) instead of Math.Pow. << is tricky again since max shift in C# is 63!
A solution for any integral values (with any valid number of bits for signed and unsigned values) is shown in Closer.CheckPrint with optional checking of the values and calculations and optional printing of the formula transformation steps.
A 'real-world' function, always checking the range of the input values and without printing is Closer.Check.
Both of them internally work with doubles. You could do Closer.CheckPrint without all the different cases and transformations. But it's done like this as a preparation for the following integral internal calculations.
More efficency is gained by doing integral calculations internally. Due to the range of the input values, you need two functions, one for signed values (Closer.I - I standing for integer), working internally with long and one for unsigned values (Closer.N - N standing for natural), working internally with ulong.
Closer.N is simpler because no overflow is possible in the delta-calculations.
For Closer.I as written below, it should be noted that the order of evaluation (which is from left to right for + and - in C#) and the order of operations in the two cases where _one is negative and _two is positive are critical. E.g. if you use delta_one_ = _value - _one - _two; instead of delta_one_ = _value - _two - _one; the intermediate result _value - _one would overflow if _value == 0 and _one == min. If you do the same in languages for which the evaluation order is not guranteed (e.g. C or C++), you have to do the calculation in two steps (e.g. delta_one_ = _value - _two; delta_one_ -= _one;).
The compiler can do the input type checking if you want to check for less than 8 byte inputs. This is shown by Closer.[IN][1248] for signed (I) and unsigned (N) inputs with 1, 2, 4, or 8 bytes. All of them call Closer.[IN], just converting the return value for less than 8 bytes.
Closer.I8 just calls Closer.I and Closer.N8 just calles Closer.N without any further ado. So you could also drop Closer.I and Closer.N and insert the code in Closer.I8 and Closer.N8 respectively. But I think it's clearer to have a function for every possible byte-count and one functions working for all byte-counts.
I don't have a local compiler at hand but from online checking the code should work.
using System;
using System.Linq;
class Closer
{
// integral signed (_bits < -1) or unsigned (_bits > 0) values
// with specified number of bits (including sign bit for signed values)
// optionally check input range and print steps
public static double CheckPrint(
double _one, double _two, double _value, long _bits, out bool _is_closer,
bool _check = true, bool _print = true)
{
double min_ = 0, max_ = 0;
Helper.BitsMinMax(_bits, out min_, out max_);
int indent_ = 2;
// check range of inputs:
Helper.PrintRange(_one, min_, max_, _print, 1, "_one", 1);
Helper.CheckIntegerRange(_one, min_, max_, _check);
Helper.PrintRange(_two, min_, max_, _print, 0, "_two", 1);
Helper.CheckIntegerRange(_two, min_, max_, _check);
Helper.PrintRange(_value, min_, max_, _print, 0, "_value", 2);
Helper.CheckIntegerRange(_value, min_, max_, _check);
// ? shortcut
if (_one == _two)
{
Helper.Print(_print, 0, "_one == _two", 2);
_is_closer = false;
return 0;
}
// ! _one != _two
// ? swap values to ensure _one < _two
if (_two < _one)
{
Helper.Print(_print, 0, "swap _one _two", 2);
double tmp_ = _one;
_one = _two;
_two = tmp_;
}
// ? shortcut: _value <= _one < _two
if (_value <= _one)
{
Helper.Print(_print, 0, "_value <= _one < _two", 2);
_is_closer = true;
return _one;
}
// ? shortcut: _one < _two <= _value
if (_value >= _two)
{
Helper.Print(_print, 0, "_one < _value <= _two", 2);
_is_closer = true;
return _two;
}
// ! _one != _two and _one <= _value <= _two
double delta_one_;
double delta_two_;
// ? 0 <= _one <= _value <= _two or _one <= _value <= two < 0:
if (_one >= 0 || _two < 0)
{
// ! same sign: no overflow possible:
Helper.Print(_print, 0,
"0 <= _one <= _value <= _two or _one <= _value <= two < 0", 2);
// edge case _one == min, _value == -1: max
Helper.PrintRange(_value - _one, min_, max_, _print,
0, "delta_one_", 1, 1 * indent_);
Helper.CheckRange(_value - _one, min_, max_, _check);
// edge case _two == max, _value == 0: max
Helper.PrintRange(_two - _value, min_, max_, _print,
0, "delta_two_", 2, 1 * indent_);
Helper.CheckRange(_two - _value, min_, max_, _check);
delta_one_ = _value - _one;
delta_two_ = _two - _value;
}
// ? _one < 0 <= _two
else
{
Helper.Print(_print, 0, "_one < 0 <= _two", 2);
// ! different sign: overflow possible!
// ? delta_two_ is save
if (_value >= 0)
{
Helper.Print(_print, 0, "_value >= 0", 2, 1 * indent_);
// fail: _one == min, _value >= 0
double test_delta_one_ = _value - _one;
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "0 delta_one_", 1, 2 * indent_);
// save: _two == max, _value == 0 (edge case)
double test_delta_two_ = _two - _value;
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "0 delta_two_", 2, 2 * indent_);
Helper.CheckRange(test_delta_two_, min_, max_, _check);
// decrease both deltas by save delta_two_
// (so delta_two_ becomes obviously 0):
test_delta_one_ = test_delta_one_ - test_delta_two_;
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "1 delta_one_", 1, 3 * indent_);
test_delta_one_ = (_value - _one) - (_two - _value);
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "2 delta_one_", 1, 3 * indent_);
test_delta_one_ = _value - _one - _two + _value;
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "3 delta_one_", 1, 3 * indent_);
test_delta_one_ = 2 * _value - _one - _two;
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "4 delta_one_", 1, 3 * indent_);
test_delta_two_ = 0;
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "1 delta_two_", 2, 3 * indent_);
// decrease both deltas by _value
test_delta_one_ = 2 * _value - _one - _two - _value;
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "5 delta_one_", 1, 4 * indent_);
Helper.CheckRange(test_delta_one_, min_, max_, _check);
test_delta_one_ = _value - _one - _two;
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "6 delta_one_", 1, 4 * indent_);
Helper.CheckRange(test_delta_one_, min_, max_, _check);
test_delta_two_ = -_value;
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "2 delta_two_", 2, 4 * indent_);
Helper.CheckRange(test_delta_two_, min_, max_, _check);
delta_one_ = _value - _two - _one;
delta_two_ = -_value;
}
// ? delta_one_ is save
else
{
Helper.Print(_print, 0, "_value < 0", 2, 1 * indent_);
// save: _one == min, _value == -1 (edge case)
double test_delta_one_ = _value - _one;
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "0 delta_one_", 1, 2 * indent_);
Helper.CheckRange(test_delta_one_, min_, max_, _check);
// fail: _two == max, _value < 0
double test_delta_two_ = _two - _value;
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "0 delta_two_", 2, 2 * indent_);
// decrease both deltas by save delta_one_
// (so delta_one_ becomes obviously 0):
test_delta_one_ = 0;
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "1 delta_one_", 1, 3 * indent_);
Helper.CheckRange(test_delta_one_, min_, max_, _check);
test_delta_two_ = test_delta_two_ - test_delta_one_;
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "1 delta_two_", 1, 3 * indent_);
test_delta_two_ = (_two - _value) - (_value - _one);
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "2 delta_two_", 1, 3 * indent_);
test_delta_two_ = _two - _value - _value + _one;
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "3 delta_two_", 1, 3 * indent_);
test_delta_two_ = _two + _one - 2 * _value;
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "4 delta_two_", 2, 3 * indent_);
// increase both deltas by _value:
test_delta_one_ = _value;
Helper.PrintRange(test_delta_one_, min_, max_,
_print, 0, "2 delta_one_", 1, 4 * indent_);
Helper.CheckRange(test_delta_one_, min_, max_, _check);
test_delta_two_ = _two + _one - 2 * _value + _value;
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "5 delta_two_", 1, 4 * indent_);
Helper.CheckRange(test_delta_two_, min_, max_, _check);
test_delta_two_ = _two + _one - _value;
Helper.PrintRange(test_delta_two_, min_, max_,
_print, 0, "6 delta_two_", 2, 4 * indent_);
Helper.CheckRange(test_delta_two_, min_, max_, _check);
delta_one_ = _value;
delta_two_ = _one - _value + _two;
}
}
if (delta_one_ < delta_two_)
{
_is_closer = true;
return _one;
}
if (delta_two_ < delta_one_)
{
_is_closer = true;
return _two;
}
_is_closer = false;
return 0;
}
// integral signed (_bits < -1) or unsigned (_bits > 0) values
// with specified number of bits (including sign bit for signed values):
public static double Check(
double _one, double _two, double _value, long _bits, out bool _is_closer)
{
double min_ = 0, max_ = 0;
Helper.BitsMinMax(_bits, out min_, out max_);
// check range of inputs:
Helper.CheckIntegerRange(_one, min_, max_, true);
Helper.CheckIntegerRange(_two, min_, max_, true);
Helper.CheckIntegerRange(_value, min_, max_, true);
if (_two < _one)
{
double tmp_ = _one;
_one = _two;
_two = tmp_;
}
double delta_one_ = _value - _one;
double delta_two_ = _two - _value;
if (delta_one_ < delta_two_)
{
_is_closer = true;
return _one;
}
if (delta_two_ < delta_one_)
{
_is_closer = true;
return _two;
}
_is_closer = false;
return 0;
}
// all signed ('I' for integer) integral types:
public static long I(
long _one, long _two, long _value, out bool _is_closer)
{
// ? shortcut
if (_one == _two)
{
_is_closer = false;
return 0;
}
// ! _one != _two
// ? swap values to ensure _one < _two
if (_two < _one)
{
_one ^= _two;
_two ^= _one;
_one ^= _two;
}
// ? shortcut: _value <= _one < _two
if (_value <= _one)
{
_is_closer = true;
return _one;
}
// ? shortcut: _one < _two <= _value
if (_value >= _two)
{
_is_closer = true;
return _two;
}
// ! _one != _two and _one < _value < _two
long delta_one_;
long delta_two_;
// ? 0 <= _one <= _value <= _two or _one <= _value <= two < 0:
if (_one >= 0 || _two < 0)
{
// same sign: no overflow possible
delta_one_ = _value - _one;
delta_two_ = _two - _value;
}
// ? _one < 0 <= _two
else
{
// different sign: overflow possible!
// ? delta_two_ is save
if (_value >= 0)
{
// to both deltas: - delta_two_ - _value
delta_two_ = -_value;
// min = -(max + 1) and min <= _one <= -1 and 0 <= _two <= max
// and 0 <= _value < _two and evaluation from left to right:
// -max <= _value - _two <= -1 and -1 - max <= _one <= -1
// -max + 1 <= (-max .. -1) - (-1 - max .. -1) <= max
delta_one_ = _value - _two - _one;
}
// ? delta_one_ is save
else
{
// to both deltas: - delta_one_ + _value
delta_one_ = _value;
// min = -(max + 1) and min <= _one <= -1 and 0 <= _two <= max
// _one < _value <= -1 and evaluation from left to right:
// -max <= _one - _value <= -1 and 0 <= _two <= max
// -max <= (-max .. -1) + (0 .. max) <= max - 1
delta_two_ = _one - _value + _two;
}
}
if (delta_one_ < delta_two_)
{
_is_closer = true;
return _one;
}
if (delta_two_ < delta_one_)
{
_is_closer = true;
return _two;
}
_is_closer = false;
return 0;
}
// all unsigned ('N' for natural) integral types:
public static ulong N(
ulong _one, ulong _two, ulong _value, out bool _is_closer)
{
// ? shortcut
if (_one == _two)
{
_is_closer = false;
return 0;
}
// ! _one != _two
// ? swap values to ensure _one < _two
if (_two < _one)
{
_one ^= _two;
_two ^= _one;
_one ^= _two;
}
// ? shortcut: _value <= _one < _two
if (_value <= _one)
{
_is_closer = true;
return _one;
}
// ? shortcut: _one < _two <= _value
if (_value >= _two)
{
_is_closer = true;
return _two;
}
// ! _one != _two and _one <= _value <= _two
ulong delta_one_ = _value - _one;
ulong delta_two_ = _two - _value;
if (delta_one_ < delta_two_)
{
_is_closer = true;
return _one;
}
if (delta_two_ < delta_one_)
{
_is_closer = true;
return _two;
}
_is_closer = false;
return 0;
}
public static sbyte I1(
sbyte _one, sbyte _two, sbyte _value, out bool _is_closer)
{
return (sbyte)I(_one, _two, _value, out _is_closer);
}
public static short I2(
short _one, short _two, short _value, out bool _is_closer)
{
return (short)I(_one, _two, _value, out _is_closer);
}
public static int I4(
int _one, int _two, int _value, out bool _is_closer)
{
return (int)I(_one, _two, _value, out _is_closer);
}
public static long I8(
long _one, long _two, long _value, out bool _is_closer)
{
return I(_one, _two, _value, out _is_closer);
}
public static byte N1(
byte _one, byte _two, byte _value, out bool _is_closer)
{
return (byte)N(_one, _two, _value, out _is_closer);
}
public static ushort N2(
ushort _one, ushort _two, ushort _value, out bool _is_closer)
{
return (ushort)N(_one, _two, _value, out _is_closer);
}
public static uint N4(
uint _one, uint _two, uint _value, out bool _is_closer)
{
return (uint)N(_one, _two, _value, out _is_closer);
}
public static ulong N8(
ulong _one, ulong _two, ulong _value, out bool _is_closer)
{
return N(_one, _two, _value, out _is_closer);
}
}
Here are the Helper class and some sample calls in Test.Main.
public class Helper
{
public static void BitsMinMax(long _bits, out double _min, out double _max)
{
// ? unsigned range with at least one value bit
if (_bits > 0)
{
_max = Math.Pow(2, _bits) - 1;
_min = 0;
return;
}
// ? signed range with one sign bit and at least one value bit
if (_bits < -1)
{
_min = -Math.Pow(2, -_bits - 1);
_max = -1 - _min;
return;
}
throw new System.ArgumentOutOfRangeException("_bits", _bits,
String.Format("-64 <= _bits <= -2 or 1 <= _bits <= 64"));
}
public static void CheckRange(
double _value, double _min, double _max, bool _check = true)
{
if (_check && (_value < _min || _value > _max))
{
throw new System.ArgumentOutOfRangeException(
"_value", _value, ValueRange2String(_value, _min, _max));
}
}
public static void CheckIntegerRange(
double _value, double _min, double _max, bool _check = true)
{
CheckRange(_value, _min, _max, _check);
if (!(_value < 0 && Math.Ceiling(_value) == _value
|| _value >= 0 && Math.Floor(_value) == _value))
{
throw new System.ArgumentException(
String.Format("_value ({0}) must be integral", _value));
}
}
public static string ValueRange2String(
double _value, double _min, double _max)
{
string format_;
if (_value < _min)
{
format_ = "_value ({0}) < _min ({1}) <= _max ({2})";
}
else if (_value > _max)
{
format_ = "_min ({1}) <= _max ({2}) < _value ({0})";
}
else
{
format_ = "_min ({1}) <= _value ({0}) <= _max ({2})";
}
return String.Format(format_, _value, _min, _max);
}
public static void Print(bool _print = true,
int _head_lines = 0, string _text = "", int _tail_lines = 0,
int _indent_count = 0, string _indent_string = " ")
{
if (_print)
{
Console.Write(String.Format("{0}{1}{2}{3}",
new String ('\n', _head_lines),
string.Concat(Enumerable.Repeat(_indent_string, _indent_count)),
_text, new String ('\n', _tail_lines)));
}
}
public static void PrintRange(
double _value, double _min, double _max, bool _print = true,
int _head_lines = 0, string _text = "", int _tail_lines = 0,
int _indent_count = 0, string _indent_string = " ")
{
if (_print)
{
if (_text != "")
{
_text = String.Format("{0}: ", _text);
}
Print(true, _head_lines, String.Format("{0}{1}",
_text, ValueRange2String(_value, _min, _max)), _tail_lines,
_indent_count, _indent_string);
}
}
public static void PrintCheckRange(
double _value, double _min, double _max, bool _print = true,
int _head_lines = 0, string _text = "", int _tail_lines = 0,
int _indent_count = 0, string _indent_string = " ", bool _check = true)
{
PrintRange(_value, _min, _max, _print,
_head_lines, _text, _tail_lines, _indent_count, _indent_string);
CheckRange(_value, _min, _max, _check);
}
}
class Test
{
public static void Main()
{
bool is_closer_;
Helper.Print(true, 1, String.Format("{0} {1}",
Closer.CheckPrint(-8, 7, 6, -4, out is_closer_), is_closer_), 1);
Helper.Print(true, 1, String.Format("{0} {1}",
Closer.Check(-8, 7, 6, -4, out is_closer_), is_closer_), 1);
Helper.Print(true, 1, String.Format("{0} {1}",
Closer.CheckPrint(15, 0, 14, 4, out is_closer_), is_closer_), 1);
Helper.Print(true, 1, String.Format("{0} {1}",
Closer.Check(15, 0, 14, 4, out is_closer_), is_closer_), 1);
Helper.Print(true, 1, String.Format("{0} {1}",
Closer.I1(-128, 127, 126, out is_closer_), is_closer_), 1);
Helper.Print(true, 1, String.Format("{0} {1}",
Closer.N2(0, 254, 255, out is_closer_), is_closer_), 1);
}
}
Compare(0, 21). This should produce zero because zero is closer, not because the distances are the same. Basically the value "zero" is being overloaded to mean both "this is the closer value" and "there is no closer value". A good follow-up exercise would be to design a better method that does not have this problem. \$\endgroup\$BigIntegertype inSystem.Numericswhich can represent arbitrarily large integers. How would you write your method if it tookBigIntegers instead ofints? Bonus: how can you write the code so that it is obviously a solution to the stated problem? The biggest problem with your original code, aside from its incorrectness, is that even if it were correct, it is impossible to glance at it and understand that it is correct immediately. \$\endgroup\$