The simplest approach is to use half-open ranges. This way, your upper bound points one step after the last valid item in the array, though your lower bound points directly to the first item. However, during the search, you treat your range as inclusive - the out-of-range upper bound is a valid "no match found" result.
At the start of each iteration you have...
lower <= target <= upper
"target" means the index that will be found and returned.
You calculate mid as "(upper + lower) / 2". Since this truncates, mid can never be the same as upper, which is important. Since "upper" may be out-of-bounds, we can never legally evaluate "array [upper]".
To find the first item greater-than-or-equal-to the key...
if array[mid] >= k : lower <= target <= mid
if array[mid] < k : mid+1 <= target <= upper
To find the first item greater-than the key...
if array[mid] > k : lower <= target <= mid
if array[mid] <= k : mid+1 <= target <= upper
These subranges are inclusive, and must precisely meet but not overlap. A single item overlap at mid (an easy mistake) results in infinite loops, which is part of why we use mid+1 for one subrange.
Note that all that changes between the two searches is the comparison operator.
To find last-less-or-equal, find first-greater and subtract one from the result. You can get -1 if all items in the array are greater than the key.
Note - you only test key against mid in each iteration (you know that the lower and upper bounds are correct already) and you only do one conditional test.
Do the out-of-bounds check and equality check (if that's what you need) outside of the loop.
int find_first_ge (int key)
{
int lower = 0;
int upper = array.length;
while (upper > lower)
{
int mid = (lower + upper) / 2;
if (array [mid] >= key) // for find_first_gt, use ">" here
{
upper = mid;
}
else
{
lower = mid + 1;
}
}
return lower;
}
NOTE
Edited to correct some mistakes that left this just as infinite-loopy as what it was meant to fix.
The trick is to ensure that the bisected subranges are exactly as needed after each key test, and always at least one smaller than the original full range - and through overconfidence and bad memory, that's exactly what I managed to get wrong. The above is based on real working code in a heavily-used library (searches within a node in a multiway-tree library), so if it's wrong I've got big problems ;-)
NOTE
Edited again to improve wording and simplify the subrange bounds descriptions (noting that although the ranges are half-open, they are treated as inclusive).