statkit
You're building statkit, the number-crunching core of a grade book. It's a
small library of pure functions — mean, percentage, roundToInt, roundTo,
clampScore, letterGrade, safeLength, weightedMean — the kind of math a
teacher runs a hundred times a term. Tiny as it is, almost every function is a
landmine for the one mistake Chapter 10 exists to cure: a conversion the
compiler performs silently is not always the conversion you meant.
The headline bug is integer-division truncation. You know the mean of a
7-point total over 2 students is 3.5 — so when mean(7, 2) returns 3, the
error isn't abstract, it's wrong arithmetic you can see. The fix is never "add a
cast somewhere"; it's putting a static_cast at exactly the right spot so the
division happens in double, not int. Other functions drill the rest of the
chapter: documenting an intentional float→int narrowing, taming the unsigned
size_t that .length() hands you, leaning on the usual arithmetic
conversions when an int meets a double, and using using aliases and
auto to keep the code readable. (In CS6340, these are the exact judgment
calls you make every time an LLVM API hands you an unsigned, a size_t, or a
line number and you have to decide what type the expression should really become.)
Your tasks
mean(total, count)→Average. Returntotal / countas a real average.mean(7, 2)must be 3.5. Convert one operand todoublebefore the divide — casting the result is too late, the truncation already happened.percentage(part, whole)→Percent.percentage(1, 4)must be 25.0. Same trap, and watch the order: do the division in floating point, then scale by100.0.roundToInt(value)→int. Round to the nearest int, half away from zero (2.5→3,-2.5→-3), then cast.static_cast<int>alone truncates; the cast also documents the intentional float→int narrowing.roundTo(value, places)→double. Round toplacesdecimals via scale → round (reuse Task 3) → unscale. Useautofor the scale-factor local.roundTo(3.14159, 2)is 3.14.clampScore(raw)→Score. Clamp into[0, 100](below→0.0, above→100.0). TheScorealias is justdouble, so the fractional part survives.letterGrade(score)→char.>=90→'A',>=80→'B',>=70→'C',>=60→'D', else'F'. Boundaries go to the higher grade. Return acharliteral — the return type beingchar(notint) is the point.safeLength(text)→int..length()returns an unsigned size type; return it as a signedintwith astatic_castthat documents the signed/unsigned conversion.safeLength("")is 0.weightedMean(p0,w0, p1,w1, p2,w2)→Average. Compute(p0*w0 + p1*w1 + p2*w2) / (w0+w1+w2). Theint*doubleproducts convert via the usual arithmetic conversions — so here you need no cast at all.
Success criteria
mean(7, 2) == 3.5andmean(-7, 2) == -3.5— the integer-division trap, signed toopercentage(1, 4) == 25.0andpercentage(1, 8) == 12.5— fractional percents surviveroundToInt(2.5) == 3androundToInt(-2.5) == -3— rounds half away from zero, not truncateroundTo(2.675, 1) == 2.7,roundTo(-1.2345, 2) == -1.23— decimal rounding, negativesclampScore(150.0) == 100.0,clampScore(99.9) == 99.9— clamp without losing the fractionletterGrade(90.0) == 'A',letterGrade(60.0) == 'D'— the band boundariessafeLength("ab") - safeLength("abcd") == -2— the subtraction that would wrap if left unsignedweightedMean(90,0.2, 80,0.3, 100,0.5) == 92.0—int*doublevia the usual arithmetic conversions
Concepts practiced
- Integer vs floating-point division and the truncation trap (
7 / 2 == 3) (10.1, 10.3) static_castto force a real-valued division and to document a narrowing (10.4, 10.6)- Numeric conversions & data-loss categories — float→int, unsigned→signed (10.3)
- Narrowing awareness: when truncation is intentional, say so with a cast (10.4)
- The usual arithmetic conversions when
intanddoublemix under an operator (10.5) - Integral promotion of
chartointduring arithmetic — and keeping acharachar(10.2) - Type aliases with
using(Score,Percent,Average) for readable, one-place-maintainable types (10.7) autofor a local whose type is obvious from its initializer (10.8)- Reused from earlier chapters: functions / headers / header guard (Ch 2),
double/bool/char/if(Ch 4),constexpr&std::string_view(Ch 5), the conditional?:, relational/logical operators, and epsilon float comparison (Ch 6)
Constraints
- Allowed:
static_cast, theusingaliases already declared,auto(Task 4), the conditional?:,if/return, relational/arithmetic operators,charliterals, and thestd::string_view/ function machinery already in the files. - Required idioms (from the notes):
static_castBEFORE the divide in Tasks 1 & 2 (not on the result).static_castto document the float→int narrowing in Task 3 and the unsigned→signed conversion in Task 7.autofor the scale-factor local in Task 4 (obvious type from initializer).- No cast in Task 8 — adding one would lie about a narrowing that isn't there.
- Forbidden (not taught yet or against the lesson): any loop (
for/while— Chapter 8; the grader is the loop), C-style casts like(int)x(notes 10.6 — usestatic_cast),<cmath>'sstd::round(do the rounding by hand so you see the conversion), containers, classes. Scattering casts just to silence warnings is also out — a cast is a promise; make it true. - Keep every function pure: read the parameters, return a value — no I/O, no globals, no side effects.
Build & run locally
make # compile-check your starter/statkit.cpp (warning-clean)
make test # grade your code -> RED until the TASK blocks are filled in
make solution # run the grader against the reference solution
make clean # remove build artifacts(make run is an alias for make test here — for this lab, "running" your code
is running the grader against it, since the grader supplies main.)
Hints
Task 1 — convert before you divide
return static_cast<double>(total) / count;Once the left operand is double, the usual arithmetic conversions promote
count to double too, so the whole division is real-valued. Writing
static_cast<double>(total / count) is the classic mistake — the int / int
truncates first, then you cast 3 to 3.0.
Task 2 — do the division in floating point, then scale
return static_cast<double>(part) / whole * 100.0;(part / whole) as ints is 0 for 1/4, and 0 * 100 is still 0. Cast part
so the division is real before you multiply by 100.0.
Task 3 — nudge by 0.5, in the value's direction
return (value >= 0.0) ? static_cast<int>(value + 0.5)
: static_cast<int>(value - 0.5);static_cast<int> truncates toward zero, so add 0.5 for positives and subtract
0.5 for negatives to round half away from zero (2.5→3, -2.5→-3).
Task 4 — auto for the obvious local; build the factor without a loop
const double powers[] { 1.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0 };
auto factor { powers[places] }; // auto deduces double
return roundToInt(value * factor) / factor;auto is the right call here — the initializer makes the type obvious. Dividing
the int from roundToInt by the double factor converts it back to double.
(The powers[] table is a C-style array — formally Chapter 17 — fine to copy as
given; or build the factor by multiplying 10.0 the right number of times.)
Task 5 — a nested conditional
return (raw < 0.0) ? 0.0 : (raw > 100.0) ? 100.0 : raw;The Score return type is just double, so returning raw keeps its fractional
part intact — aliases rename a type, they don't narrow it.
Task 6 — a char ladder
return (score >= 90.0) ? 'A'
: (score >= 80.0) ? 'B'
: (score >= 70.0) ? 'C'
: (score >= 60.0) ? 'D'
: 'F';Each test is strictly above the next, so order resolves the boundaries (exactly
90 → 'A'). You return a char literal directly — no arithmetic, so nothing
promotes it to int.
Task 7 — one documented cast
return static_cast<int>(text.length());.length() is unsigned; the static_cast is your promise that the string is
short enough to fit in int, and it silences the signed/unsigned warning an
implicit conversion would raise.
Task 8 — let the conversions work for you (no cast)
return (p0 * w0 + p1 * w1 + p2 * w2) / (w0 + w1 + w2);In every int * double product the int is converted to double automatically,
so numerator and denominator are both double. Adding a static_cast here would
falsely imply a narrowing that never happens.
Stretch goals
- Make the aliases type-safe: an alias is not a distinct type, so
PercentandScoreare interchangeable today. Wrap one in a tinystruct/enum classso the compiler rejects mixing them (needs classes/enums, Chapters 13–14). - Replace the
powers[]table inroundTowith aconstexprhelper, and have it rejectplacesoutside0..6withassert(assert is Chapter 9). - Add
meanOf(const std::vector<int>& xs)that sums with a loop and divides as adoubleaverage — the same truncation trap, now over a real container (loops Ch 8, vectors Ch 16). - Add a
formatPercent(Percent)returning a 1-decimalstd::stringand watch the float-formatting issues the notes warn about (std::setprecision, Chapter 28). - CS6340 tie-in: write
auto *p { … }-style code where a pretend API returns a pointer, and ausing CoverageSet = std::set<std::pair<int,int>>;alias to see how aliases tame noisy LLVM types (notes 10.7 / 10.8).
// Chapter 10 — Type Conversion, Aliases, and Deduction · statkit (STARTER)
// ─────────────────────────────────────────────────────────────────────────────
// Fill in the eight TASK blocks below. Each maps 1:1 to a task in the README and
// to a declaration in ../statkit.h. The bodies currently return PLACEHOLDERS so
// the file compiles immediately — that is why `make test` is RED right now. Many
// of the placeholders even reproduce the CLASSIC BUG on purpose (e.g. mean does
// integer division and returns 3 for 7/2), so the failing checks point straight
// at the conversion you need to fix. Your job is to turn the wall of red GREEN.
//
// make build compile-check your code (should already work)
// make test grade it (RED until you fill these in)
// make solution run the reference if you get stuck
//
// No loops, no I/O, no globals — just get the CONVERSIONS in the right places.
#include "../statkit.h"
// (No extra headers are needed. Rounding here is done with + 0.5 and a cast, not
// std::round — the point of the chapter is to see the conversion happen.)
namespace statkit
{
// ─── TASK 1: mean — fix the integer-division truncation ───────────────────
// RIGHT NOW this does `total / count` with two ints, so mean(7, 2) returns 3.0
// (the .5 was thrown away by integer division before it ever became a double).
// Convert ONE operand to double BEFORE dividing so the division is real-valued:
// static_cast<double>(total) / count
// (Once one side is double, `count` is promoted and the result is double.)
//
// >>> YOUR CODE HERE <<<
//
Average mean(int total, int count)
{
return total / count; // placeholder — INTEGER division, so 7/2 == 3, not 3.5
}
// ─── TASK 2: percentage — real division, on a 0–100 scale ─────────────────
// percentage(1, 4) must be 25.0. Integer `part / whole` is 0, and 0 * 100 is
// still 0, so converting AFTER the divide is too late. Do the division in
// floating point, e.g.: static_cast<double>(part) / whole * 100.0
//
// >>> YOUR CODE HERE <<<
//
Percent percentage(int /*part*/, int /*whole*/)
{
return 0.0; // placeholder
}
// ─── TASK 3: roundToInt — round half away from zero, then cast ────────────
// static_cast<int>(value) TRUNCATES toward zero (2.9 -> 2, -2.9 -> -2); it does
// not round. To round to nearest, nudge by 0.5 in the direction of the value's
// sign, THEN cast. One clean way that also handles negatives:
// value >= 0 ? static_cast<int>(value + 0.5)
// : static_cast<int>(value - 0.5)
// The cast is intentional and documents the float→int narrowing.
//
// >>> YOUR CODE HERE <<<
//
int roundToInt(double /*value*/)
{
return 0; // placeholder
}
// ─── TASK 4: roundTo — scale, round, unscale (use `auto` for the factor) ──
// Round to `places` decimals. Compute 10^places as a double, scale the value
// up, round to nearest with roundToInt (Task 3), then scale back down:
// auto factor { ... }; // a double like 100.0 for places==2
// return roundToInt(value * factor) / factor;
// `auto` is appropriate here — the type is obvious from the initializer (10.8).
// Build factor without a loop: for places 0..6 you may multiply 10.0 the right
// number of times, or simply switch on the small set of `places` values.
//
// >>> YOUR CODE HERE <<<
//
double roundTo(double value, int /*places*/)
{
return value; // placeholder — returns value unrounded
}
// ─── TASK 5: clampScore — clamp into [0, 100], return as the Score alias ──
// Below 0 -> 0.0; above 100 -> 100.0; otherwise unchanged. The `Score` return
// type is just `double`, so returning the (possibly fractional) value loses
// nothing. A nested ?: reads cleanly:
// raw < 0.0 ? 0.0 : raw > 100.0 ? 100.0 : raw
//
// >>> YOUR CODE HERE <<<
//
Score clampScore(double /*raw*/)
{
return 0.0; // placeholder
}
// ─── TASK 6: letterGrade — return the right `char` for the band ───────────
// >= 90 -> 'A', >= 80 -> 'B', >= 70 -> 'C', >= 60 -> 'D', else 'F'. Boundaries
// go to the higher grade (exactly 90 is 'A'). Return a char LITERAL directly —
// a nested ?: or an if/return ladder both work. (Note: 'A' is a char, but if
// you did 'A' + 1 it would promote to int; you don't need arithmetic here.)
//
// >>> YOUR CODE HERE <<<
//
char letterGrade(Percent /*score*/)
{
return '?'; // placeholder
}
// ─── TASK 7: safeLength — unsigned size_t length -> signed int, documented ─
// text.length() returns an UNSIGNED size type. Return it as a plain `int` with
// a static_cast that documents the intentional signed/unsigned conversion:
// static_cast<int>(text.length())
// (Returning .length() straight into an int would trip a signedness warning;
// the cast is your promise that the range is small enough to be safe.)
//
// >>> YOUR CODE HERE <<<
//
int safeLength(std::string_view /*text*/)
{
return 0; // placeholder
}
// ─── TASK 8: weightedMean — let the usual arithmetic conversions work ─────
// (p0*w0 + p1*w1 + p2*w2) / (w0 + w1 + w2). Each `int * double` product
// converts the int to double automatically (usual arithmetic conversions,
// 10.5), so the whole numerator and denominator are doubles and the result is
// a double Average. No casts are needed BECAUSE a double is already present in
// every product — write the formula directly.
//
// >>> YOUR CODE HERE <<<
//
Average weightedMean(int /*p0*/, double /*w0*/,
int /*p1*/, double /*w1*/,
int /*p2*/, double /*w2*/)
{
return 0.0; // placeholder
}
}
Try the lab first — the learning is in the attempt.
// Chapter 10 — Type Conversion, Aliases, and Deduction · statkit (REFERENCE)
// ─────────────────────────────────────────────────────────────────────────────
// One complete, correct, warning-clean implementation. Peek only after you've
// taken a real swing at starter/statkit.cpp — the learning is in finding where
// each conversion belongs, then comparing.
//
// Theme: every function below places a conversion deliberately. Where a cast is
// needed it is `static_cast` (loud, auditable, intentional); where the usual
// arithmetic conversions already do the right thing (Task 8) there is NO cast,
// because a needless cast is also a lie about what the code is doing.
#include "../statkit.h"
namespace statkit
{
// TASK 1 — mean.
// The fix for integer-division truncation: convert ONE operand to double
// BEFORE the divide. Once the left operand is double, the usual arithmetic
// conversions promote `count` to double too, so the division is real-valued
// and 7 / 2 yields 3.5. Casting the *result* (static_cast<double>(total/count))
// would be too late — the truncation already happened in int.
Average mean(int total, int count)
{
return static_cast<double>(total) / count;
}
// TASK 2 — percentage.
// Same trap, with operand order to watch: the division must be floating point.
// We cast `part` to double so `part / whole` is real-valued, then scale by
// 100.0. percentage(1, 4) -> 0.25 -> 25.0.
Percent percentage(int part, int whole)
{
return static_cast<double>(part) / whole * 100.0;
}
// TASK 3 — roundToInt.
// static_cast<int> truncates toward zero, so to round to nearest we nudge by
// 0.5 in the value's own direction first. This keeps symmetry for negatives:
// 2.5 -> 3 and -2.5 -> -3 (round half away from zero). The cast documents the
// intentional float→int narrowing (notes 10.4 / 10.6).
int roundToInt(double value)
{
return (value >= 0.0)
? static_cast<int>(value + 0.5)
: static_cast<int>(value - 0.5);
}
// TASK 4 — roundTo.
// Scale up, round to nearest whole, scale back down. `auto` is used for the
// scale factor because the initializer makes the type (double) obvious — the
// idiomatic place for type deduction (notes 10.8). We build 10^places by
// repeated multiplication over the small, bounded range 0..6 (no loop: a tiny
// table). roundToInt returns int; dividing it by the double `factor` converts
// it back to double via the usual arithmetic conversions.
double roundTo(double value, int places)
{
// 10^places for places in 0..6, as a double. (constexpr-friendly lookup.)
const double powers[] { 1.0, 10.0, 100.0, 1000.0,
10000.0, 100000.0, 1000000.0 };
auto factor { powers[places] }; // auto deduces double from the array element
return roundToInt(value * factor) / factor;
}
// TASK 5 — clampScore.
// A nested conditional that pins the value into [0, 100]. The `Score` return
// type is an alias for double, so the fractional part survives untouched —
// aliases rename, they do not narrow (notes 10.7).
Score clampScore(double raw)
{
return (raw < 0.0) ? 0.0
: (raw > 100.0) ? 100.0
: raw;
}
// TASK 6 — letterGrade.
// A relational ladder returning a char literal per band; boundaries resolve to
// the higher grade because each test is strictly above the next. The return
// TYPE is char — the right-sized type for a single letter — which is the
// chapter point: a char literal is a char, and we keep it that way (no
// arithmetic that would promote it to int).
char letterGrade(Percent score)
{
return (score >= 90.0) ? 'A'
: (score >= 80.0) ? 'B'
: (score >= 70.0) ? 'C'
: (score >= 60.0) ? 'D'
: 'F';
}
// TASK 7 — safeLength.
// std::string_view::length() returns an unsigned size_type. We convert it to a
// signed int with an explicit static_cast: the cast is a documented promise
// that the text is short enough that the value fits, and it silences the
// signed/unsigned warning we'd otherwise get from an implicit conversion
// (notes 10.3 / 10.6 decision table).
int safeLength(std::string_view text)
{
return static_cast<int>(text.length());
}
// TASK 8 — weightedMean.
// The usual arithmetic conversions do the work: in each `int * double` product
// the int operand is converted to double, so numerator and denominator are
// both double and the quotient is a double Average. No static_cast is needed —
// and adding one would falsely imply a narrowing that isn't happening.
Average weightedMean(int p0, double w0,
int p1, double w1,
int p2, double w2)
{
return (p0 * w0 + p1 * w1 + p2 * w2) / (w0 + w1 + w2);
}
}
// Chapter 10 — Type Conversion, Aliases, and Deduction · statkit (GRADER)
// ─────────────────────────────────────────────────────────────────────────────
// A tiny no-framework unit-test harness (same style as the drills/CLAUDE.md
// spec). It includes ../statkit.h and calls each function across MANY inputs —
// THIS is the "loop" you don't have to write. Each CHECK that fails prints its
// expression and line number. Any failure -> non-zero exit -> `make test` is RED.
//
// Calculated doubles carry tiny representation error, so we never compare them
// with `==` (notes 6.7 / 10). CLOSE(a, b) asks "within a small epsilon?" instead.
// The exact-value checks (roundToInt, letterGrade, safeLength) return int/char,
// which DO compare exactly.
//
// The Makefile links this file against starter/statkit.cpp (your code) for
// `make test`, and against solution/statkit.cpp for `make test-solution`.
#include <iostream>
#include <cmath> // std::fabs — for the epsilon comparison
#include <string_view>
#include "../statkit.h"
static int fails = 0;
// CHECK: assert a boolean condition; on failure, report what and where.
#define CHECK(cond) \
do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
// CLOSE: the SAFE way to compare two doubles (never use == on calculated ones).
static bool close(double a, double b)
{
return std::fabs(a - b) <= 1e-9;
}
int main()
{
using namespace statkit;
// ── Task 1: mean — the integer-division truncation trap ──────────────────
CHECK(close(mean(7, 2), 3.5)); // THE trap: must be 3.5, not 3
CHECK(close(mean(10, 4), 2.5)); // 10/4 == 2 in int division; want 2.5
CHECK(close(mean(6, 3), 2.0)); // exact case still works
CHECK(close(mean(1, 3), 0.3333333333)); // repeating fraction, real division
CHECK(close(mean(0, 5), 0.0)); // edge: total 0
CHECK(close(mean(-7, 2), -3.5)); // edge: negative total stays fractional
// ── Task 2: percentage — same trap + operand order ───────────────────────
CHECK(close(percentage(1, 4), 25.0)); // int 1/4 would be 0 -> 0%
CHECK(close(percentage(1, 2), 50.0));
CHECK(close(percentage(3, 4), 75.0));
CHECK(close(percentage(0, 10), 0.0)); // edge: 0%
CHECK(close(percentage(7, 7), 100.0)); // edge: full marks
CHECK(close(percentage(1, 8), 12.5)); // fractional percent survives
// ── Task 3: roundToInt — rounds, does not truncate; handles negatives ────
CHECK(roundToInt(2.4) == 2);
CHECK(roundToInt(2.5) == 3); // half rounds UP (truncation would give 2)
CHECK(roundToInt(2.6) == 3);
CHECK(roundToInt(3.0) == 3); // exact integer
CHECK(roundToInt(-2.5) == -3); // edge: half away from zero, not -2
CHECK(roundToInt(-2.4) == -2); // edge: negative rounds toward zero here
CHECK(roundToInt(0.0) == 0); // edge: zero
// ── Task 4: roundTo — decimal rounding via scale/round/unscale ───────────
CHECK(close(roundTo(3.14159, 2), 3.14));
CHECK(close(roundTo(3.14559, 2), 3.15)); // rounds the 3rd place up
CHECK(close(roundTo(2.5, 0), 3.0)); // edge: 0 places == roundToInt
CHECK(close(roundTo(1.0 / 3.0, 3), 0.333));
CHECK(close(roundTo(2.675, 1), 2.7)); // rounds to one decimal
CHECK(close(roundTo(-1.2345, 2), -1.23)); // edge: negative value
// ── Task 5: clampScore — clamp into [0,100], keep fractional part ────────
CHECK(close(clampScore(87.5), 87.5)); // in range -> unchanged (and fractional)
CHECK(close(clampScore(0.0), 0.0));
CHECK(close(clampScore(100.0), 100.0));
CHECK(close(clampScore(-5.0), 0.0)); // edge: below floor -> 0
CHECK(close(clampScore(150.0), 100.0)); // edge: above ceiling -> 100
CHECK(close(clampScore(99.9), 99.9)); // just under the ceiling
// ── Task 6: letterGrade — char ladder, boundaries to the higher grade ────
CHECK(letterGrade(95.0) == 'A');
CHECK(letterGrade(90.0) == 'A'); // boundary -> higher grade
CHECK(letterGrade(89.9) == 'B'); // just below
CHECK(letterGrade(80.0) == 'B'); // boundary
CHECK(letterGrade(70.0) == 'C'); // boundary
CHECK(letterGrade(60.0) == 'D'); // boundary
CHECK(letterGrade(59.9) == 'F');
CHECK(letterGrade(0.0) == 'F'); // edge: zero
// ── Task 7: safeLength — unsigned size_t length -> signed int ────────────
CHECK(safeLength("hello") == 5);
CHECK(safeLength("") == 0); // edge: empty view
CHECK(safeLength("a") == 1);
CHECK(safeLength("cs6340") == 6);
// The whole point: the result is a signed int we can safely do signed math on.
CHECK(safeLength("ab") - safeLength("abcd") == -2); // would WRAP if left unsigned
// ── Task 8: weightedMean — usual arithmetic conversions (int*double) ─────
// Equal weights reduce to the plain mean of the points.
CHECK(close(weightedMean(80, 1.0, 90, 1.0, 100, 1.0), 90.0));
// Skewed weights pull the result toward the heavily-weighted point.
// (90*0.2 + 80*0.3 + 100*0.5) / (0.2+0.3+0.5) = (18+24+50)/1.0 = 92.0
CHECK(close(weightedMean(90, 0.2, 80, 0.3, 100, 0.5), 92.0));
// Fractional result: (70*1 + 75*1 + 0*0) / 2 = 72.5
CHECK(close(weightedMean(70, 1.0, 75, 1.0, 0, 0.0), 72.5));
// Single non-zero weight returns that point exactly (no int truncation).
CHECK(close(weightedMean(7, 1.0, 0, 0.0, 0, 0.0), 7.0));
if (!fails)
std::cout << "PASS ✅ all statkit checks passed.\n";
else
std::cerr << "\nFAIL ❌ " << fails << " check(s) failed — fix the TASK blocks in statkit.cpp.\n";
return fails ? 1 : 0;
}
# Chapter 10 — Type Conversion / Aliases / auto · statkit · unit-test grader (Style B).
# Targets follow the drills/CLAUDE.md Makefile contract. TABS, not spaces.
#
# The learner implements ../statkit.h in starter/statkit.cpp. There is no main()
# in statkit.cpp on purpose — the grader (tests/tests.cpp) supplies main() and
# calls every function across many inputs, so the learner never writes a loop.
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra
.PHONY: all build run test solution test-solution clean
all: build
# build — compile-check the learner's code (warning-clean object file, no link).
# There is no program to link yet because tests/ provides main(); `make test`
# does the full build+run. This target just proves your statkit.cpp compiles.
build:
$(CXX) $(CXXFLAGS) -c starter/statkit.cpp -o starter/statkit.o
@echo "OK ✅ starter/statkit.cpp compiles. Now run: make test"
# run — for this lab, "running" the code IS running the grader against it.
run: test
# test — grade the LEARNER's code: link the grader against starter/statkit.cpp.
# RED until the TASK blocks are filled in; GREEN once they're correct.
test:
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/statkit.cpp -o tests/run
@./tests/run
# solution — build+run the grader against the reference implementation.
solution: test-solution
# test-solution — proof the lab is solvable: the reference MUST pass every check.
test-solution:
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/statkit.cpp -o tests/run
@./tests/run
clean:
rm -f starter/statkit.o tests/run
rm -rf tests/run.dSYM
// Chapter 10 — Type Conversion, Aliases, and Deduction · Project: statkit
// ─────────────────────────────────────────────────────────────────────────────
// This header is the CONTRACT between you and the grader, and it is also the
// chapter lesson in one page. DO NOT EDIT IT. The grader (tests/tests.cpp) and
// BOTH starter/statkit.cpp (yours) and solution/statkit.cpp (the reference)
// include it; change a signature here and nothing links.
//
// `statkit` is a tiny grade-book statistics library. Every function it promises
// makes ONE Chapter-10 idea PHYSICAL — something you can watch go wrong and then
// fix:
//
// • mean(7, 2) must be 3.5, NOT 3 ← the integer-division truncation trap
// • percentage(1, 4) must be 25.0 ← same trap, plus operand ORDER
// • roundToInt(2.5) must be 3 ← intentional float→int narrowing, via cast
// • clampScore / letterGrade ← `using` aliases + char from int
// • safeLength(".length()") ← the unsigned size_t → int conversion
// • weightedMean(int pts, double w) ← the usual ARITHMETIC conversions
//
// The headline lesson of the whole chapter lives in this file: a conversion that
// the compiler will happily perform is not always the conversion you MEANT. The
// fix is to put a `static_cast` at exactly the right spot — no more, no fewer.
//
// "Pure" functions: each reads only its arguments and returns a value. No input,
// no output, no globals, no loops (loops are Chapter 8 — and the grader supplies
// the repetition by calling each function across many inputs).
#ifndef STATKIT_H // ── header guard (Chapter 2) ───────────────────
#define STATKIT_H // include this file's contents at most once per TU
#include <string_view> // std::string_view — a cheap read-only text view (Ch 5)
// ─────────────────────────────────────────────────────────────────────────────
// TYPE ALIASES (notes 10.7) — `using NewName = ExistingType;`
//
// An alias gives an existing type a second, more meaningful NAME. It reads
// left-to-right and is the modern replacement for `typedef`. Crucially, an alias
// does NOT create a new, distinct type: `Score` below is *exactly* `double`, so
// a `Score` and a plain `double` are freely interchangeable. The payoff is
// READABILITY and one-place MAINTENANCE — if grades ever needed more precision
// you'd change `double` here once, not in fifty signatures.
//
// We use these aliases throughout the API so the signatures document intent:
// a function returning an `Average` clearly produces a real-valued average, even
// though the compiler just sees `double`.
// ─────────────────────────────────────────────────────────────────────────────
using Score = double; // a single graded value (e.g. a points-out-of-100 score)
using Percent = double; // a value on the 0–100 percentage scale
using Average = double; // the real-valued result of an averaging computation
namespace statkit
{
// ─── TASK 1: mean — the integer-division truncation trap ──────────────────
// Return the arithmetic mean of `total` divided over `count` items, as an
// Average (double). Both inputs are int. The trap: `total / count` with two
// ints does INTEGER division and throws away the remainder, so 7 / 2 is 3,
// not 3.5. You must convert to double BEFORE the divide (a static_cast on one
// operand promotes the whole expression). Assume count > 0.
Average mean(int total, int count);
// ─── TASK 2: percentage — same trap, and operand ORDER matters ────────────
// Return part/whole expressed on a 0–100 scale, as a Percent (double).
// percentage(1, 4) is 25.0. If you compute `(part / whole) * 100` with int
// division you get 0 * 100 == 0; if you write `part / whole * 100.0` the int
// division STILL happens first. Convert to floating point at the right spot so
// the division is real-valued. Assume whole > 0.
Percent percentage(int part, int whole);
// ─── TASK 3: roundToInt — INTENTIONAL float→int narrowing, documented ──────
// Round a double to the nearest int (round half away from zero) and return it
// as an int. Plain static_cast<int>(2.5) TRUNCATES to 2 — it does not round.
// Add (or subtract, for negatives) 0.5 before the cast. The static_cast is the
// POINT here: it announces "I know this discards the fractional part; that is
// intentional," which a silent `int x = value;` would not. Handle negatives:
// roundToInt(-2.5) must be -3, not -2.
int roundToInt(double value);
// ─── TASK 4: roundTo — build on Task 3; use `auto` for an obvious local ────
// Round `value` to `places` decimal places and return the double. Strategy:
// scale up by 10^places, round to the nearest whole number, scale back down.
// Reuse roundToInt for the middle step. Use `auto` (notes 10.8) for the
// scale factor local, where the type is obvious from the initializer.
// roundTo(3.14159, 2) is 3.14. Assume places is 0..6.
double roundTo(double value, int places);
// ─── TASK 5: clampScore — narrowing-aware clamp, returns an alias ─────────
// Clamp a raw score into the valid 0–100 range and return it as a Score.
// Below 0 → 0.0; above 100 → 100.0; otherwise the value unchanged. The input
// is a double (it may carry fractional points); the return type is the `Score`
// alias (which IS double — no information is lost). One ?: or two ifs.
Score clampScore(double raw);
// ─── TASK 6: letterGrade — a char ladder; `char` interacts with `int` ─────
// Map a Percent (0–100) to a single letter grade and return it as a `char`:
// >= 90 -> 'A', >= 80 -> 'B', >= 70 -> 'C', >= 60 -> 'D', else 'F'.
// Char literals like 'A' have type char, but the moment you do arithmetic on
// them they PROMOTE to int (notes 10.2) — so returning a char from an int-y
// expression would narrow. Here you just return the right char literal, but
// the return type being `char` (not int) is the lesson: pick the type that
// fits the value. Take the boundary cases (exactly 90, 80, …) as the higher
// grade.
char letterGrade(Percent score);
// ─── TASK 7: safeLength — the unsigned size_t → int conversion (notes 10.3)─
// std::string_view::length() returns a size_type, which is UNSIGNED. Mixing it
// with signed ints triggers signed/unsigned warnings and, on subtraction, the
// wrap-to-huge trap. Return the length as a plain signed `int`, using a
// static_cast to DOCUMENT that you know the source is unsigned and the range
// is small enough to be safe (notes 10.6 decision table). safeLength("") is 0.
int safeLength(std::string_view text);
// ─── TASK 8: weightedMean — the usual ARITHMETIC conversions (notes 10.5) ──
// Compute a weighted mean: points are int, weights are double. Return the
// Average. For three (point, weight) pairs:
// (p0*w0 + p1*w1 + p2*w2) / (w0 + w1 + w2)
// When an int and a double meet under an operator, the int is converted to
// double and the result is double (the "usual arithmetic conversions"). So
// here the mixing works in your favor — but only because at least one operand
// of every product is already a double. Assume the weights sum to > 0.
Average weightedMean(int p0, double w0,
int p1, double w1,
int p2, double w2);
}
#endif // STATKIT_H
make test locally
(see “Build & run locally” above).