Numeric Types Lab
The fundamental types — int, unsigned, the fixed-width integers, double,
bool, char — all look like plain numbers until they bite you: a counter
wraps to zero, 0.1 + 0.2 refuses to equal 0.3, a negative index turns into a
gigantic positive one. This lab makes that behavior physical. You implement
six small functions, and the grader proves the types behave exactly as the notes
warned — overflow wraps, chars are numbers in disguise, floats are approximate,
and signed→unsigned casts need a guard.
Each function isolates one idea from Chapter 4, and the tasks ramp up: Task 1
is a one-liner about sizeof; Task 6 is a small if/else-if classifier that
ties bool logic and branches back to the CS6340 "why" (every if is a
control-flow path that coverage analysis measures). The tests assert only
platform-independent facts — fixed-width sizes and relationships, never an
absolute sizeof(int) — so a green result means the same thing on every machine.
Your tasks
int32ByteWidth()— return how many bytes astd::int32_toccupies. Use thesizeofoperator (it already yields astd::size_t).shiftChar(char ch, int delta)— shiftchbydeltain its encoding. Convert char→int, add, convert int→char; usestatic_castfor both. E.g.shiftChar('A', 1) == 'B'.wrapsAround(uint8_t start, uint8_t addend)— returntrueexactly whenstart + addendoverflows an 8-bit unsigned (true sum> 255). Observe the real wrap; don't use signed overflow.nearlyEqual(double a, double b, double epsilon)— return|a - b| <= epsilon. Never compare doubles with==.isValidIndex(int index, size_t length)— returntrueiff0 <= index < length. Checkindex >= 0first, thenstatic_casttostd::size_tand compare withlength.classifyNumber(double value)(capstone) — return achartag using an orderedif/else-ifchain:'Z'if (nearly) zero,'N'if negative,'B'if>= 1000.0,'P'otherwise. Reuse yournearlyEqualfor the zero test.
Success criteria
make test prints PASS ✅ all checks. It compiles
tests/tests.cpp against your starter/numeric_lab.cpp and
runs ~30 CHECKs covering every function plus edge cases — the 255 + 1
overflow boundary, the 0.1 + 0.2 != 0.3 float trap, a negative index, and the
ordering of the classifier. Until you fill in the tasks, the placeholders fail
and you'll see FAIL: <expr> @line N for each, then FAIL ❌. Turning that red
into green is the whole exercise. (Our proof it's solvable:
make test-solution is green.)
Concepts practiced
sizeofand the fixed-width integersstd::int32_t/std::uint8_t(4.3, 4.6)- Unsigned wraparound — defined, modular, and surprising (4.5)
- Floating-point approximation and epsilon comparison instead of
==(4.8) boolvalues andstd::boolalphaoutput (4.9)char↔intround-tripping; chars are numeric underneath (4.11)static_castfor explicit, searchable conversions; the negative-to-size_ttrap (4.12)if/else if/elsechains and ordered conditions (4.10)- Reused from earlier chapters: user-defined functions across a
.h/.cppsplit with a header guard, andreturn(Ch 2); arithmetic expressions (Ch 1)
Constraints
- Allowed:
int/unsigned/ fixed-width ints,float/double,bool,char,sizeof,static_cast, andif/else(4.10).std::absfrom<cmath>is already included for you. - Forbidden (not taught yet): loops,
switch,std::string,?:,&&/||chained logic — none are needed. Solve every task with the tools above. - Always
static_castfor conversions you intend (char↔int, signed→size_t). No C-style casts. - Guard before you cast a possibly-negative
inttostd::size_t(Task 5). - Do not edit
numeric_lab.h, the tests, or the demo — only fill instarter/numeric_lab.cpp.
Build & run locally
make # compile starter/ + demo -> starter/app
make run # run the demo: see your functions' behavior printed
make test # grade your code (unit tests) — RED until the TASKs are done
make solution # build + run the reference demo if you get stuck
make cleanHints
Task 1 — sizeof
sizeof(std::int32_t) is a std::size_t, so return sizeof(std::int32_t);
is the whole body. (It's 4 — 32 bits ÷ 8 bits/byte — on every platform that
has the type, which is why the test can hard-code == 4.)
Task 2 — char ↔ int round-trip
int code { static_cast<int>(ch) };
return static_cast<char>(code + delta);Do the + delta in int, then cast the sum back to char. Two explicit casts.
Task 3 — why a plain start + addend > 255 won't work
std::uint8_t is narrower than int, so start + addend is promoted to
int and never wraps — the comparison would always be false for the cases that
matter. Force the result back into the 8-bit type and check if it shrank:
std::uint8_t wrapped { static_cast<std::uint8_t>(start + addend) };
return wrapped < start;Task 4 — epsilon compare
return std::abs(a - b) <= epsilon;. Use <= (not <) so a gap exactly
equal to epsilon still counts as "near" — one of the edge cases is built to
check that.
Task 5 — order of operations is the lesson
if (index < 0) { return false; }
return static_cast<std::size_t>(index) < length;The index < 0 check must come first. Cast a negative int to std::size_t
before checking and it becomes a huge value that slides right past length.
Task 6 — ordered if/else-if
Test the cases in the README's order; the first match wins:
if (nearlyEqual(value, 0.0, 1e-9)) return 'Z';
else if (value < 0.0) return 'N';
else if (value >= 1000.0) return 'B';
else return 'P';Reuse your own nearlyEqual for the zero test instead of value == 0.0.
Stretch goals
- Add a
classifyNumberband for "huge" values (>= 1e6) using scientific notation literals (1e6). - Replace the
if (index < 0)guard with a single combined condition once you've met&&(Chapter 6):index >= 0 && static_cast<std::size_t>(index) < length. - Print
min/maxof each fixed-width type with<limits>(std::numeric_limits<std::int32_t>::max()), and confirmwrapsAroundagrees at the boundary. - Generalize
nearlyEqualto a relative tolerance (scaleepsilonby the magnitude of the inputs) so it works for very large values too.
// ============================================================================
// Chapter 4 — Fundamental Data Types · Project: Numeric Types Lab (STARTER)
// numeric_lab.cpp — fill in the six TASK blocks, then `make test`.
// ============================================================================
//
// This file COMPILES AS-IS (warning-clean), but every function returns a
// deliberately wrong placeholder, so `make test` starts RED. Replace each
// `>>> YOUR CODE HERE <<<` with a real body to turn it GREEN. The contract for
// each function lives in ../numeric_lab.h — read those comments first.
//
// Allowed this chapter: int / unsigned / fixed-width ints, float/double, bool,
// char, sizeof, static_cast, and if / else (4.10). NO loops, NO switch, NO
// std::string — those are later chapters.
// ----------------------------------------------------------------------------
#include "../numeric_lab.h"
#include <cmath> // std::abs(double) — handy for the floating-point tasks
// ─── TASK 1: int32ByteWidth ──────────────────────────────────────────────────
// Return how many BYTES a std::int32_t occupies. Use the `sizeof` operator
// (it already yields a std::size_t, so you can return it directly).
// Hint: `sizeof(std::int32_t)`.
//
// >>> YOUR CODE HERE <<<
//
std::size_t int32ByteWidth()
{
return 0; // placeholder — a real int32_t is never 0 bytes wide
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 2: shiftChar ───────────────────────────────────────────────────────
// Convert `ch` to int, add `delta`, convert the sum back to char, return it.
// Use static_cast for BOTH conversions (char→int, then int→char).
//
// >>> YOUR CODE HERE <<<
//
char shiftChar(char ch, int delta)
{
(void)delta; // silence "unused parameter" until you use it
return ch; // placeholder — returns the char unchanged (no shift)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 3: wrapsAround ─────────────────────────────────────────────────────
// Return true exactly when start + addend overflows an 8-bit unsigned value
// (true sum > 255). One clean way: store the wrapped result in a std::uint8_t
// and check whether it came out SMALLER than `start`. Don't use signed overflow.
//
// >>> YOUR CODE HERE <<<
//
bool wrapsAround(std::uint8_t start, std::uint8_t addend)
{
(void)start;
(void)addend;
return false; // placeholder — claims nothing ever wraps (wrong for 255+1)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 4: nearlyEqual ─────────────────────────────────────────────────────
// Return true when |a - b| <= epsilon. Never compare doubles with ==.
// std::abs(double) (from <cmath>) gives you the magnitude of the difference.
//
// >>> YOUR CODE HERE <<<
//
bool nearlyEqual(double a, double b, double epsilon)
{
(void)a;
(void)b;
(void)epsilon;
return false; // placeholder — claims nothing is ever "close enough"
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 5: isValidIndex ────────────────────────────────────────────────────
// Return true iff 0 <= index < length. CHECK index >= 0 FIRST; only then cast
// it to std::size_t and compare with length. (Casting a negative int to
// std::size_t makes a huge number — that's the bug we're guarding against.)
//
// >>> YOUR CODE HERE <<<
//
bool isValidIndex(int index, std::size_t length)
{
(void)index;
(void)length;
return true; // placeholder — claims EVERY index is valid (even -1)
}
// ─────────────────────────────────────────────────────────────────────────────
// ─── TASK 6: classifyNumber (capstone) ───────────────────────────────────────
// Return a char tag using an if / else-if / else chain, IN THIS ORDER:
// 'Z' if nearlyEqual(value, 0.0, 1e-9) (use your own Task-4 function)
// 'N' if value < 0.0
// 'B' if value >= 1000.0
// 'P' otherwise
//
// >>> YOUR CODE HERE <<<
//
char classifyNumber(double value)
{
(void)value;
return '?'; // placeholder — not one of the real tags Z/N/B/P
}
// ─────────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// ============================================================================
// Chapter 4 — Fundamental Data Types · Project: Numeric Types Lab (SOLUTION)
// numeric_lab.cpp — one complete, correct reference implementation.
// ============================================================================
//
// Peek only after you've tried your own version — you learn the types by
// *fighting* them, not by reading the answer. Every function is warning-clean
// under `clang++ -std=c++17 -Wall -Wextra`.
// ----------------------------------------------------------------------------
#include "../numeric_lab.h"
#include <cmath> // std::abs(double)
// ─── TASK 1: int32ByteWidth ──────────────────────────────────────────────────
std::size_t int32ByteWidth()
{
// `sizeof` reports the size of a TYPE in bytes and already evaluates to a
// std::size_t, so we can hand it straight back. For std::int32_t this is 4
// on every conforming platform (32 bits / 8 bits-per-byte) — a fixed-width
// guarantee plain `int` does NOT give us.
return sizeof(std::int32_t);
}
// ─── TASK 2: shiftChar ───────────────────────────────────────────────────────
char shiftChar(char ch, int delta)
{
// A char is an integer underneath. Promote it to int EXPLICITLY, do the
// arithmetic in int (where +/- behave normally), then cast the sum back to
// char. The two static_casts document the char↔int round-trip; the caller
// promised the result stays in valid char range, so no validation needed.
int code { static_cast<int>(ch) };
int shifted { code + delta };
return static_cast<char>(shifted);
}
// ─── TASK 3: wrapsAround ─────────────────────────────────────────────────────
bool wrapsAround(std::uint8_t start, std::uint8_t addend)
{
// Unsigned arithmetic is modular and DEFINED: an 8-bit unsigned holds
// 0..255, so the sum naturally "wraps" past 255 back toward 0.
//
// Caution: std::uint8_t is narrower than int, so `start + addend` would be
// promoted to int and would NOT wrap. To observe the real 8-bit wraparound
// we force the result back into a std::uint8_t. If that wrapped result is
// smaller than where we started, the true sum must have exceeded 255.
std::uint8_t wrapped { static_cast<std::uint8_t>(start + addend) };
return wrapped < start;
}
// ─── TASK 4: nearlyEqual ─────────────────────────────────────────────────────
bool nearlyEqual(double a, double b, double epsilon)
{
// The right way to compare approximate values: ask whether they're within a
// tolerance, never `a == b`. std::abs gives the magnitude of the gap.
return std::abs(a - b) <= epsilon;
}
// ─── TASK 5: isValidIndex ────────────────────────────────────────────────────
bool isValidIndex(int index, std::size_t length)
{
// Guard the sign FIRST. A negative int cast to std::size_t becomes a huge
// positive number, which would sail past almost any length check — the
// exact signed→unsigned trap from the notes. Only once we know index >= 0
// is it safe to convert and compare against the unsigned length.
if (index < 0)
{
return false;
}
return static_cast<std::size_t>(index) < length;
}
// ─── TASK 6: classifyNumber (capstone) ───────────────────────────────────────
char classifyNumber(double value)
{
// An if / else-if / else chain: conditions are tested top-to-bottom and the
// FIRST match wins, so order matters. Each branch below is a distinct
// control-flow path — precisely what coverage analysis exercises.
if (nearlyEqual(value, 0.0, 1e-9)) // reuse Task 4 instead of value == 0.0
{
return 'Z';
}
else if (value < 0.0)
{
return 'N';
}
else if (value >= 1000.0)
{
return 'B';
}
else
{
return 'P';
}
}
// ============================================================================
// Chapter 4 — Numeric Types Lab · demo driver (for `make run` / `make solution`).
// ============================================================================
// Not a grader — just prints what your functions DO, so the type behavior is
// physical: watch an unsigned value wrap, a char become an int and back, and a
// classification chain pick a branch. Links against starter/ or solution/.
// ----------------------------------------------------------------------------
#include <iostream>
#include "../numeric_lab.h"
int main()
{
std::cout << std::boolalpha; // print bools as true/false, not 1/0 (4.9)
std::cout << "== Numeric Types Lab ==\n\n";
std::cout << "Task 1 sizeof(int32) ....... " << int32ByteWidth()
<< " bytes\n";
std::cout << "Task 2 shiftChar('A', 1) ... '" << shiftChar('A', 1)
<< "' (char<->int round-trip)\n";
std::cout << "Task 3 wrapsAround(255, 1) . " << wrapsAround(255, 1)
<< " (8-bit unsigned 255+1 -> 0)\n";
std::cout << "Task 4 0.1 + 0.2 ~= 0.3 .... "
<< nearlyEqual(0.1 + 0.2, 0.3, 1e-9)
<< " (== would say " << (0.1 + 0.2 == 0.3) << "!)\n";
std::cout << "Task 5 isValidIndex(-1, 5) . " << isValidIndex(-1, 5)
<< " (negative guarded before cast)\n";
std::cout << "Task 6 classifyNumber(2500) '" << classifyNumber(2500.0)
<< "' (Z/N/B/P branch tags)\n";
std::cout << "\n(Run `make test` to grade these against the spec.)\n";
return 0;
}
// ============================================================================
// Chapter 4 — Numeric Types Lab · unit-test grader (no framework).
// ============================================================================
// Links against either starter/numeric_lab.cpp (→ RED) or
// solution/numeric_lab.cpp (→ GREEN). See the Makefile.
//
// Every assertion below is a PLATFORM-INDEPENDENT fact: we test fixed-width
// widths and RELATIONSHIPS, never absolute sizeof(int)/sizeof(long) values
// (those legitimately differ across machines).
// ----------------------------------------------------------------------------
#include <iostream>
#include "../numeric_lab.h"
static int fails = 0;
// Print the offending expression + line on failure; keep counting.
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
int main()
{
// ── Task 1: int32ByteWidth ───────────────────────────────────────────────
// 32 bits / 8 bits-per-byte = 4 bytes, guaranteed for the fixed-width type.
CHECK(int32ByteWidth() == 4);
// Relationship sanity: a 32-bit value is exactly four 8-bit bytes wide.
CHECK(int32ByteWidth() == sizeof(std::uint8_t) * 4);
// ── Task 2: shiftChar (char ↔ int round-trip) ────────────────────────────
CHECK(shiftChar('A', 1) == 'B'); // 65 -> 66
CHECK(shiftChar('A', 0) == 'A'); // identity shift
CHECK(shiftChar('z', -25) == 'a'); // walk back down the alphabet
CHECK(shiftChar('5', 1) == '6'); // digit chars are numeric too ('5'->'6')
// Edge: a *negative* delta must move DOWN the encoding ('a'=97 -> '`'=96).
CHECK(shiftChar('a', -1) == '`');
// ── Task 3: wrapsAround (unsigned 8-bit modular arithmetic) ───────────────
CHECK(wrapsAround(255, 1) == true); // 255 + 1 -> 0 : wraps
CHECK(wrapsAround(200, 100) == true); // 300 > 255 : wraps
CHECK(wrapsAround(100, 50) == false); // 150 fits : no wrap
CHECK(wrapsAround(0, 0) == false); // 0 fits : no wrap
// Edge: adding 0 to the maximum must NOT report a wrap.
CHECK(wrapsAround(255, 0) == false);
// Edge: the smallest possible overflow, 255 + 1, sits right on the boundary.
CHECK(wrapsAround(254, 1) == false); // 255 exactly : still fits
// ── Task 4: nearlyEqual (floating-point tolerance) ───────────────────────
CHECK(nearlyEqual(1.0, 1.0, 1e-9) == true); // identical
CHECK(nearlyEqual(1.0, 1.5, 1e-9) == false); // far apart
CHECK(nearlyEqual(2.0, 2.0 + 1e-12, 1e-9) == true); // within tolerance
// THE classic trap: 0.1 + 0.2 is NOT exactly 0.3 in binary floating point...
CHECK((0.1 + 0.2 == 0.3) == false); // proof the trap is real
// ...but nearlyEqual sees through it.
CHECK(nearlyEqual(0.1 + 0.2, 0.3, 1e-9) == true);
// Edge: a difference EXACTLY equal to epsilon counts as "near" (<=, not <).
// We use exactly-representable values (1.0, 1.5, 0.5) so the boundary is
// crisp — at larger magnitudes 5.0+1e-9 would round to *just over* epsilon,
// which is itself the floating-point lesson, but a brittle thing to assert.
CHECK(nearlyEqual(1.0, 1.5, 0.5) == true);
// ...and just past it (epsilon a hair too small) is NOT near.
CHECK(nearlyEqual(1.0, 1.5, 0.4) == false);
// ── Task 5: isValidIndex (guard signed→unsigned cast) ────────────────────
CHECK(isValidIndex(0, 5) == true); // first slot
CHECK(isValidIndex(4, 5) == true); // last valid slot
CHECK(isValidIndex(5, 5) == false); // one past the end
CHECK(isValidIndex(0, 0) == false); // empty container: nothing is valid
// Edge: the whole point — a negative index must be rejected, NOT cast into a
// gigantic std::size_t that would slip past the length check.
CHECK(isValidIndex(-1, 5) == false);
// ── Task 6: classifyNumber (bool logic + if/else-if, branch coverage) ─────
CHECK(classifyNumber(0.0) == 'Z'); // (nearly) zero
CHECK(classifyNumber(1e-12) == 'Z'); // within 1e-9 of zero -> still 'Z'
CHECK(classifyNumber(-3.5) == 'N'); // negative
CHECK(classifyNumber(42.0) == 'P'); // ordinary positive
CHECK(classifyNumber(2500.0) == 'B'); // big positive (>= 1000)
// Edge: exactly 1000.0 is the boundary into 'B' (>=, not >).
CHECK(classifyNumber(1000.0) == 'B');
// Edge: ordering matters — -1e-12 is negative, but it sits within the zero
// epsilon (1e-9), and the 'Z' test runs FIRST in the chain — so 'Z' beats 'N'.
CHECK(classifyNumber(-1e-12) == 'Z');
if (!fails)
{
std::cout << "PASS ✅ all checks (" << "Numeric Types Lab" << ")\n";
}
return fails ? 1 : 0;
}
# Chapter 4 — Numeric Types Lab · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. Recipe lines use TABS.
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra
HEADER := numeric_lab.h
.PHONY: all build run test solution test-solution clean
all: build
# `make` / `make build` — compile the STARTER implementation (proves it builds).
build: starter/app
starter/app: starter/numeric_lab.cpp $(HEADER) tests/demo.cpp
$(CXX) $(CXXFLAGS) tests/demo.cpp starter/numeric_lab.cpp -o starter/app
# `make run` — run the starter demo so you can see your functions' behavior.
run: build
./starter/app
# `make test` — grade the LEARNER's starter code. RED until the TASK blocks are done.
test: $(HEADER) tests/tests.cpp starter/numeric_lab.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp starter/numeric_lab.cpp -o tests/run
@./tests/run && echo "PASS ✅ starter passes — nice work." || echo "FAIL ❌ fill in the TASK blocks in starter/numeric_lab.cpp until every CHECK passes."
# `make solution` — build + run the reference demo.
solution: solution/app
./solution/app
solution/app: solution/numeric_lab.cpp $(HEADER) tests/demo.cpp
$(CXX) $(CXXFLAGS) tests/demo.cpp solution/numeric_lab.cpp -o solution/app
# `make test-solution` — grade the SOLUTION. MUST be green (our proof it's solvable).
test-solution: $(HEADER) tests/tests.cpp solution/numeric_lab.cpp
@$(CXX) $(CXXFLAGS) tests/tests.cpp solution/numeric_lab.cpp -o tests/run
@./tests/run && echo "PASS ✅ solution passes all checks." || echo "FAIL ❌ solution does NOT pass — this is a bug in the exercise."
clean:
rm -f starter/app solution/app tests/run
// ============================================================================
// Chapter 4 — Fundamental Data Types · Project: Numeric Types Lab
// numeric_lab.h — the public API you implement (DECLARATIONS ONLY).
// ============================================================================
//
// This header is COMPLETE — do not edit it. It declares six small functions.
// You write their bodies in `numeric_lab.cpp`; the grader in `tests/` calls
// these exact signatures and checks the results.
//
// The whole point of this lab is to make the behavior of the *fundamental
// types* PHYSICAL — you'll observe overflow wrap to a small number, a `char`
// turn into an `int` and back, two `double`s that "look equal" but aren't, and
// a signed→unsigned cast that you must guard before it bites you.
//
// A header guard keeps this file from being included twice in one translation
// unit (Chapter 2). `#pragma once` does the same job in one line.
// ----------------------------------------------------------------------------
#ifndef NUMERIC_LAB_H
#define NUMERIC_LAB_H
#include <cstdint> // std::uint8_t, std::int32_t — FIXED-WIDTH integers (4.6)
#include <cstddef> // std::size_t — the unsigned type sizeof/.size() return (4.3, 4.6)
// ─── TASK 1 ─────────────────────────────────────────────────────────────────
// How many BYTES does one `std::int32_t` occupy?
//
// We deliberately ask about a FIXED-WIDTH type. Unlike plain `int` (whose size
// the standard only bounds as "at least 16 bits"), `std::int32_t` is *exactly*
// 32 bits wherever it exists — so its byte count is the same on every platform
// the course targets. That makes this a PLATFORM-INDEPENDENT fact a test can
// assert. Use the `sizeof` operator; it yields a `std::size_t`.
std::size_t int32ByteWidth();
// ─── TASK 2 ─────────────────────────────────────────────────────────────────
// Shift a character by `delta` positions in its encoding and return the result.
//
// A `char` is a number underneath (4.11): in ASCII, 'A' is 65, 'a' is 97. To do
// arithmetic on it cleanly you convert char→int, add, then convert int→char on
// the way back — a CHAR↔INT ROUND-TRIP. Example: shiftChar('A', 1) == 'B';
// shiftChar('a', -1) == '`'. Use `static_cast` for BOTH conversions so the
// intent is explicit and searchable. The caller guarantees the result stays in
// valid char range, so you don't need to validate here.
char shiftChar(char ch, int delta);
// ─── TASK 3 ─────────────────────────────────────────────────────────────────
// Does adding `addend` to the 8-bit unsigned value `start` WRAP AROUND?
//
// Unsigned arithmetic is modular (4.5): an 8-bit unsigned holds 0..255, and
// 255 + 1 becomes 0 — a defined "wraparound", NOT undefined behavior. Return
// true exactly when the true mathematical sum start+addend would exceed 255
// (i.e. the stored `std::uint8_t` result is smaller than `start`). Do the test
// WITHOUT signed overflow and WITHOUT just trusting the wrapped value blindly.
// Edge: start=255, addend=1 → wraps (returns true). start=255, addend=0 → no.
bool wrapsAround(std::uint8_t start, std::uint8_t addend);
// ─── TASK 4 ─────────────────────────────────────────────────────────────────
// Are two doubles equal WITHIN a tolerance `epsilon`?
//
// Floating-point values are approximate (4.8): 0.1 + 0.2 is NOT exactly 0.3, so
// comparing doubles with `==` is a classic bug. Instead, two values are "close
// enough" when the magnitude of their difference is at most `epsilon`. Return
// |a - b| <= epsilon. (You may use std::abs from <cmath>, already included in
// the .cpp; or build the absolute value yourself with an `if`.)
bool nearlyEqual(double a, double b, double epsilon);
// ─── TASK 5 ─────────────────────────────────────────────────────────────────
// Is `index` a valid position into a container of length `length`?
//
// `length` is a `std::size_t` (UNSIGNED — what .size()/sizeof give you). The
// caller's `index` is a signed `int`, which might be negative. The trap (4.12):
// casting a negative `int` to `std::size_t` yields a HUGE positive number, so
// you must check `index >= 0` FIRST, and only then compare it against `length`.
// Return true iff 0 <= index < length. When you finally compare to `length`,
// use a `static_cast<std::size_t>(index)` so signed/unsigned don't collide.
bool isValidIndex(int index, std::size_t length);
// ─── TASK 6 (capstone) ──────────────────────────────────────────────────────
// Classify a double using bool logic + an if / else-if chain. Return a CHAR tag:
// 'Z' if the value is (nearly) zero — within 1e-9 of 0.0
// 'N' if it is negative — strictly less than 0
// 'B' if it is a "big" positive — >= 1000.0
// 'P' otherwise (an ordinary positive)
// Check the cases IN THIS ORDER (4.10: conditions are tested top-to-bottom until
// one matches). Reuse your nearlyEqual for the zero test. Every `if` here is a
// control-flow BRANCH — exactly what coverage analysis in CS6340 measures.
char classifyNumber(double value);
#endif // NUMERIC_LAB_H
make test locally
(see “Build & run locally” above).