From cac4e01d2a12c164767808c0ea9dc61b9ecf3e85 Mon Sep 17 00:00:00 2001 From: jenner-analytics <299455446+jenner-analytics@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:23:19 +0000 Subject: [PATCH] jenner-check: add 5 Jenner compatibility bundles + runner Adds jenner-check/, five self-contained bundles exercising macros from common-macros.sas, me-macros.sas, and nme-margin-calculation.sas against mock antidumping sales data (CMAC2_COUNTER, DEFINE_SALE_DATE, UNIQUE_CONNUMS, CREATE_QUARTERS, CONVERT_TO_USD), plus the run_jenner.sh runner and its README. Nothing outside jenner-check/ is touched and nothing runs on merge or checkout. --- jenner-check/.gitignore | 2 + jenner-check/README.md | 76 ++++ jenner-check/run_jenner.sh | 357 ++++++++++++++++++ jenner-check/t001_cmac2_counter/autoexec.sas | 2 + jenner-check/t001_cmac2_counter/expected.json | 13 + .../t001_cmac2_counter/expected/log.txt | 23 ++ .../t001_cmac2_counter/expected/output.txt | 0 jenner-check/t001_cmac2_counter/meta.json | 7 + jenner-check/t001_cmac2_counter/script.sas | 45 +++ .../t002_define_sale_date/autoexec.sas | 2 + .../t002_define_sale_date/expected.json | 16 + .../t002_define_sale_date/expected/log.txt | 13 + .../t002_define_sale_date/expected/output.txt | 9 + jenner-check/t002_define_sale_date/meta.json | 7 + jenner-check/t002_define_sale_date/script.sas | 46 +++ jenner-check/t003_unique_connums/autoexec.sas | 2 + .../t003_unique_connums/expected.json | 17 + .../t003_unique_connums/expected/log.txt | 31 ++ .../t003_unique_connums/expected/output.txt | 7 + jenner-check/t003_unique_connums/meta.json | 7 + jenner-check/t003_unique_connums/script.sas | 73 ++++ .../t004_create_quarters/autoexec.sas | 2 + .../t004_create_quarters/expected.json | 16 + .../t004_create_quarters/expected/log.txt | 13 + .../t004_create_quarters/expected/output.txt | 9 + jenner-check/t004_create_quarters/meta.json | 7 + jenner-check/t004_create_quarters/script.sas | 71 ++++ jenner-check/t005_convert_to_usd/autoexec.sas | 2 + .../t005_convert_to_usd/expected.json | 15 + .../t005_convert_to_usd/expected/log.txt | 32 ++ .../t005_convert_to_usd/expected/output.txt | 17 + jenner-check/t005_convert_to_usd/meta.json | 7 + jenner-check/t005_convert_to_usd/script.sas | 106 ++++++ 33 files changed, 1052 insertions(+) create mode 100644 jenner-check/.gitignore create mode 100644 jenner-check/README.md create mode 100755 jenner-check/run_jenner.sh create mode 100644 jenner-check/t001_cmac2_counter/autoexec.sas create mode 100644 jenner-check/t001_cmac2_counter/expected.json create mode 100644 jenner-check/t001_cmac2_counter/expected/log.txt create mode 100644 jenner-check/t001_cmac2_counter/expected/output.txt create mode 100644 jenner-check/t001_cmac2_counter/meta.json create mode 100644 jenner-check/t001_cmac2_counter/script.sas create mode 100644 jenner-check/t002_define_sale_date/autoexec.sas create mode 100644 jenner-check/t002_define_sale_date/expected.json create mode 100644 jenner-check/t002_define_sale_date/expected/log.txt create mode 100644 jenner-check/t002_define_sale_date/expected/output.txt create mode 100644 jenner-check/t002_define_sale_date/meta.json create mode 100644 jenner-check/t002_define_sale_date/script.sas create mode 100644 jenner-check/t003_unique_connums/autoexec.sas create mode 100644 jenner-check/t003_unique_connums/expected.json create mode 100644 jenner-check/t003_unique_connums/expected/log.txt create mode 100644 jenner-check/t003_unique_connums/expected/output.txt create mode 100644 jenner-check/t003_unique_connums/meta.json create mode 100644 jenner-check/t003_unique_connums/script.sas create mode 100644 jenner-check/t004_create_quarters/autoexec.sas create mode 100644 jenner-check/t004_create_quarters/expected.json create mode 100644 jenner-check/t004_create_quarters/expected/log.txt create mode 100644 jenner-check/t004_create_quarters/expected/output.txt create mode 100644 jenner-check/t004_create_quarters/meta.json create mode 100644 jenner-check/t004_create_quarters/script.sas create mode 100644 jenner-check/t005_convert_to_usd/autoexec.sas create mode 100644 jenner-check/t005_convert_to_usd/expected.json create mode 100644 jenner-check/t005_convert_to_usd/expected/log.txt create mode 100644 jenner-check/t005_convert_to_usd/expected/output.txt create mode 100644 jenner-check/t005_convert_to_usd/meta.json create mode 100644 jenner-check/t005_convert_to_usd/script.sas diff --git a/jenner-check/.gitignore b/jenner-check/.gitignore new file mode 100644 index 0000000..75fb4a3 --- /dev/null +++ b/jenner-check/.gitignore @@ -0,0 +1,2 @@ +*_response.json +response.json diff --git a/jenner-check/README.md b/jenner-check/README.md new file mode 100644 index 0000000..58459d6 --- /dev/null +++ b/jenner-check/README.md @@ -0,0 +1,76 @@ +# jenner-check: compatibility test bundles + +Each `tNNN_/` subdirectory is a self-contained test bundle built from SAS +code that already lives in this repository. A bundle pins the output of a +captured passing run so the same script can be re-run later and compared +against that snapshot. + +This directory is fully self-contained: nothing outside `jenner-check/` is +referenced or modified, nothing runs on merge or checkout, and deleting the +directory removes every trace of it. + +## What the runner sends — read before running + +`run_jenner.sh` uploads only a bundle's SAS **source text** — `autoexec.sas` +plus `script.sas`, and nothing else — over HTTPS to `api.jenneranalytics.com`, +where it runs and returns the log and listing. It does not read or upload any +data files, so anything sitting next to a script stays on your machine; the +only thing transmitted is the code you run, as when pasting a snippet into any +hosted tool. Nothing is sent unless you run a command yourself. To point the +runner at a different endpoint, set `JENNER_HOST`. + +## What's in here + +``` +jenner-check/ +├── README.md # this file +├── run_jenner.sh # runner (bash + curl; python3 used if present) +└── tNNN_/ + ├── script.sas # the SAS under test (derived from this repo) + ├── autoexec.sas # run options prepended to script.sas at run time + ├── expected.json # fields pinned from the captured passing run + ├── meta.json # provenance: source file, blob sha, commit + └── expected/ # human-readable snapshot of that run + ├── log.txt + └── output.txt +``` + +`meta.json` records the source path, blob sha, and commit of the script each +bundle was built from, so you can verify the copy matches your code. + +## How to run + +From inside this directory: + +```bash +./run_jenner.sh --list # show the bundles in this directory +./run_jenner.sh --all # run every bundle, verify pinned fields +./run_jenner.sh tNNN_ # run just one (use a name from --list) +``` + +For each bundle the runner submits the SAS, writes the JSON response to +`_response.json`, and compares the response against the bundle's +`expected.json` — status, exit code, and the pinned log lines. A bundle +passes only when every pinned field matches, so an `N pass, 0 fail` summary +from `--all` means the captured results still hold. + +You can also compare a saved response offline, with no network call: + +```bash +./run_jenner.sh --compare tNNN__response.json tNNN_/expected.json +``` + +Requirements: `bash` 4+ and `curl` (both ship with mainstream Linux and +macOS); `python3` for the pinned-field comparison. On Windows, run it under +WSL. + +## Removing this directory + +`git rm -r jenner-check/` (or just delete the folder). Nothing else in the +repository references it. + +--- + +`run_jenner.sh` and this README are provided under the MIT license. Each +bundle's `script.sas` is a copy of code that already lives in this repository +and remains under this repository's own license — nothing here relicenses it. diff --git a/jenner-check/run_jenner.sh b/jenner-check/run_jenner.sh new file mode 100755 index 0000000..ad7825a --- /dev/null +++ b/jenner-check/run_jenner.sh @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +# run_jenner.sh - mac/linux runner for Jenner compatibility checks. +# +# Quick start: +# cd jenner-check/ +# ./run_jenner.sh # lists bundles in the current dir +# ./run_jenner.sh t001_something # run that one +# ./run_jenner.sh --all # run every bundle in the current dir +# +# Usage: ./run_jenner.sh [bundle-dir | script.sas | --all | --list] [response.json] +# ./run_jenner.sh --compare RESPONSE.json EXPECTED.json +# +# (no arg) If the current directory has tNNN_* bundles, list them +# with a copy-paste command. Otherwise show this help. +# +# --all Run every tNNN_* bundle in the current directory in +# sequence, print a pass/fail summary. +# +# --list, -l List the bundles visible in the current directory and +# exit without running anything. +# +# --compare Purely local, no network: check RESPONSE.json against the +# pinned EXPECTED.json and print one line per check. Only +# keys present in EXPECTED.json are checked (keys starting +# with "_" are provenance and ignored): status / exit_code +# must match exactly, every log_contains entry must appear +# in the response log, no log_does_not_contain entry may +# appear, output_contains entries must appear in the output, +# and pinned diagnostics lists must match exactly. Unknown +# keys print a warning and do not fail. +# Exit 0 all-match, 1 any mismatch, 2 usage/unreadable file/ +# invalid JSON, 5 python3 unavailable. +# +# bundle-dir A directory containing script.sas and (optionally) +# autoexec.sas. The two are concatenated (autoexec first, +# then a blank line, then script) and submitted together. +# This is the normal case. +# +# script.sas A single .sas file. Submitted as-is — no autoexec. +# +# The API response is written to (or response.json in +# the current directory if omitted) and the most useful fields are also +# printed to stdout for a quick sanity check. +# +# What "pass" means: a bundle run passes only if the POST returns HTTP 200 +# AND — when the bundle carries an expected.json and python3 is available — +# the fresh response matches every pinned field (the --compare semantics +# above). Without python3 the pinned comparison is skipped with a notice +# and the run falls back to the old HTTP/status-only behavior. +# +# Requires: bash 4+, curl. Both ship with every mainstream Linux distro +# and macOS 12+. python3 (present on virtually every modern system) is +# optional but needed for the pinned-result comparison and the pretty +# summary. Windows: use run_jenner.bat (single-file mode) or WSL. +# +# IMPORTANT: execute this script, don't source it. Running with `. ./...` +# or `source ./...` will short-circuit error handling and can close your +# terminal if an error path fires. + +# --- refuse to be sourced ------------------------------------------------ +# `return` only works inside a sourced script. If we ARE sourced, print a +# message and return 1 so we don't kill the parent shell with exit. If +# we're running directly, (return 0) fails and we fall through. +(return 0 2>/dev/null) && { + printf 'run_jenner.sh: execute this script, do not source it.\n ./run_jenner.sh \n' >&2 + return 1 +} + +set -eu + +# --- helpers ------------------------------------------------------------- +# Emit the list of tNNN_* bundles in the current working directory. A +# "bundle" is a directory matching t[0-9]*_* whose name contains a +# script.sas file. Writes one path per line (no prefix); empty output +# if nothing found. +list_bundles_here() { + local d + for d in ./t[0-9]*_*/ ; do + [[ -d "$d" && -f "$d/script.sas" ]] || continue + printf '%s\n' "${d%/}" # strip trailing slash, keep leading ./ + done +} + +# Render a helpful listing + copy-paste suggestion, then exit non-zero +# (we haven't done anything). Used when the user runs with no args. +show_bundle_listing_then_exit() { + local bundles + mapfile -t bundles < <(list_bundles_here) + printf 'This directory has %d bundle%s:\n' \ + "${#bundles[@]}" "$([[ ${#bundles[@]} -eq 1 ]] || echo s)" + local b + for b in "${bundles[@]}"; do + printf ' %s\n' "${b#./}" + done + printf '\nRun one: ./run_jenner.sh %s\n' "${bundles[0]#./}" + printf 'Run them all: ./run_jenner.sh --all\n' + printf 'Just list: ./run_jenner.sh --list\n' + exit 2 +} + +# Show the usage block when we have nothing better to offer. +show_usage_then_exit() { + local status=${1:-2} + { + printf 'Usage: %s [bundle-dir | script.sas | --all | --list] [response.json]\n' "$(basename "$0")" + printf ' %s --compare RESPONSE.json EXPECTED.json\n\n' "$(basename "$0")" + printf 'Examples:\n' + printf ' %s t001_my_bundle # run one bundle\n' "$(basename "$0")" + printf ' %s --all # run every tNNN_* bundle in this dir\n' "$(basename "$0")" + printf ' %s path/to/script.sas # run a single file, no autoexec\n' "$(basename "$0")" + printf ' %s --compare r.json e.json # check a response against pinned fields\n' "$(basename "$0")" + } >&2 + exit "$status" +} + +# Compare a response.json against a pinned expected.json. Purely local — +# no network. Only keys present in expected.json are checked; keys whose +# name starts with "_" are capture provenance and are ignored. Prints one +# line per check (ok/FAIL/warn). Return codes: 0 all checks match, +# 1 any mismatch, 2 unreadable file or invalid JSON, 5 python3 missing. +compare_response_to_expected() { + local resp_file=$1 expected_file=$2 + if ! command -v python3 >/dev/null 2>&1; then + printf 'error: the pinned comparison needs python3, which was not found\n' >&2 + return 5 + fi + python3 - "$resp_file" "$expected_file" <<'PY' +import json, sys + +def load(path, label): + try: + with open(path, encoding="utf-8") as f: + return json.load(f) + except (OSError, ValueError) as e: + print(f"compare: cannot read {label} file {path}: {e}", file=sys.stderr) + sys.exit(2) + +resp = load(sys.argv[1], "response") +exp = load(sys.argv[2], "expected") +if not isinstance(resp, dict) or not isinstance(exp, dict): + print("compare: both files must contain a JSON object", file=sys.stderr) + sys.exit(2) + +failures = 0 + +def ok(msg): + print(f" ok {msg}") + +def bad(msg): + global failures + failures += 1 + print(f" FAIL {msg}") + +def as_list(v): + return v if isinstance(v, list) else [v] + +log = resp.get("log") or "" +output = resp.get("output") or "" + +for key, want in exp.items(): + if key.startswith("_"): + continue # provenance (_captured_at, ...), not a check + if key in ("status", "exit_code"): + got = resp.get(key) + if got == want: + ok(f"{key} == {want!r}") + else: + bad(f"{key}: expected {want!r}, got {got!r}") + elif key == "log_contains": + for s in as_list(want): + if s in log: + ok(f"log contains {s!r}") + else: + bad(f"log_contains: log is missing {s!r}") + elif key == "log_does_not_contain": + for s in as_list(want): + if s not in log: + ok(f"log does not contain {s!r}") + else: + bad(f"log_does_not_contain: log unexpectedly contains {s!r}") + elif key == "output_contains": + for s in as_list(want): + if s in output: + ok(f"output contains {s!r}") + else: + bad(f"output_contains: output is missing {s!r}") + elif key == "diagnostics": + diag = resp.get("diagnostics") or {} + for dk, dv in (want or {}).items(): + if dk.startswith("_"): + continue + got = diag.get(dk) + if got == dv: + ok(f"diagnostics.{dk} == {dv!r}") + else: + bad(f"diagnostics.{dk}: expected {dv!r}, got {got!r}") + else: + print(f" warn unknown expected.json key {key!r} — not checked") + +if failures: + print(f"pinned comparison: {failures} check(s) FAILED") + sys.exit(1) +print("pinned comparison: all pinned checks passed") +PY +} + +# --- arg parsing --------------------------------------------------------- +if [[ $# -lt 1 ]]; then + # No args: if the cwd contains bundles, list them; otherwise show help. + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -gt 0 ]]; then + show_bundle_listing_then_exit + fi + show_usage_then_exit 2 +fi + +HOST=${JENNER_HOST:-api.jenneranalytics.com} + +case "$1" in + -h|--help) + show_usage_then_exit 0 + ;; + -l|--list) + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -eq 0 ]]; then + printf 'No tNNN_* bundles found in %s\n' "$(pwd)" + exit 0 + fi + printf 'Bundles in %s:\n' "$(pwd)" + for b in "${_found[@]}"; do + printf ' %s\n' "${b#./}" + done + exit 0 + ;; + --compare) + # Purely local: diff a response.json against a pinned expected.json. + if [[ $# -ne 3 ]]; then + printf 'usage: %s --compare RESPONSE.json EXPECTED.json\n' "$(basename "$0")" >&2 + exit 2 + fi + _rc=0 + compare_response_to_expected "$2" "$3" || _rc=$? + exit "$_rc" + ;; + --all) + mapfile -t _found < <(list_bundles_here) + if [[ ${#_found[@]} -eq 0 ]]; then + printf 'No tNNN_* bundles found in %s\n' "$(pwd)" >&2 + exit 3 + fi + _pass=0; _fail=0 + for b in "${_found[@]}"; do + printf '\n── %s ──\n' "${b#./}" + if "$0" "$b" "${b#./}_response.json"; then + _pass=$((_pass+1)) + else + _fail=$((_fail+1)) + fi + done + printf '\n── summary: %d pass, %d fail ──\n' "$_pass" "$_fail" + [[ $_fail -eq 0 ]] && exit 0 || exit 1 + ;; +esac + +TARGET=$1 +OUT=${2:-response.json} + +# --- assemble the submission body --------------------------------------- +# If TARGET is a directory, treat it as a bundle. If it's a file, submit +# it directly. +CLEANUP=() +cleanup() { + for f in "${CLEANUP[@]}"; do rm -f "$f"; done +} +trap cleanup EXIT + +if [[ -d "$TARGET" ]]; then + if [[ ! -f "$TARGET/script.sas" ]]; then + printf 'error: %s is a directory but has no script.sas\n' "$TARGET" >&2 + exit 3 + fi + SUBMIT=$(mktemp -t jc_submit.XXXXXX.sas) + CLEANUP+=("$SUBMIT") + if [[ -f "$TARGET/autoexec.sas" ]]; then + cat "$TARGET/autoexec.sas" > "$SUBMIT" + printf '\n' >> "$SUBMIT" + fi + cat "$TARGET/script.sas" >> "$SUBMIT" + printf 'Submitting bundle: %s\n' "$TARGET" + if [[ -f "$TARGET/autoexec.sas" ]]; then + printf ' autoexec.sas (%d bytes) + script.sas (%d bytes)\n' \ + "$(wc -c < "$TARGET/autoexec.sas")" "$(wc -c < "$TARGET/script.sas")" + else + printf ' script.sas (%d bytes), no autoexec\n' "$(wc -c < "$TARGET/script.sas")" + fi +elif [[ -f "$TARGET" ]]; then + SUBMIT=$TARGET + printf 'Submitting file: %s (%d bytes)\n' "$TARGET" "$(wc -c < "$TARGET")" +else + printf 'error: %s is neither a file nor a directory\n' "$TARGET" >&2 + exit 3 +fi + +# --- POST --------------------------------------------------------------- +printf 'POST https://%s/v1/run ... ' "$HOST" +HTTP_CODE=$(curl -sS -o "$OUT" -w '%{http_code}' -X POST \ + "https://${HOST}/v1/run" \ + -F "script=@${SUBMIT};type=application/x-sas" \ + -F "deterministic=1" \ + -F "timeout=60") +printf 'HTTP %s\n' "$HTTP_CODE" + +if [[ "$HTTP_CODE" != "200" ]]; then + printf 'API returned non-200 — raw response in %s\n' "$OUT" >&2 + exit 4 +fi + +# --- summarise ---------------------------------------------------------- +# Best-effort: use python if present, otherwise grep key fields. +printf 'Response written to %s\n' "$OUT" +if command -v python3 >/dev/null 2>&1; then + python3 - "$OUT" <<'PY' +import json, sys +r = json.load(open(sys.argv[1])) +print(f" status : {r.get('status')}") +print(f" exit_code : {r.get('exit_code')}") +print(f" duration_ms: {r.get('duration_ms')}") +print(f" run_id : {r.get('run_id')}") +print(f" jenner_ver : {r.get('jenner_version')}") +log = r.get('log', '') +if log: + print(' log (first 10 lines):') + for line in log.splitlines()[:10]: + print(f' {line}') +PY +else + printf ' (install python3 for a pretty summary; raw JSON in %s)\n' "$OUT" +fi + +# --- pinned-result comparison --------------------------------------------- +# A bundle that ships an expected.json only passes if the fresh response +# matches every pinned field — HTTP 200 alone is not a pass. Without +# python3 we can't parse JSON portably, so fall back to the old behavior +# and say so. +if [[ -d "$TARGET" && -f "$TARGET/expected.json" ]]; then + if command -v python3 >/dev/null 2>&1; then + printf 'Comparing response against pinned %s/expected.json\n' "${TARGET%/}" + _rc=0 + compare_response_to_expected "$OUT" "$TARGET/expected.json" || _rc=$? + if [[ $_rc -ne 0 ]]; then + printf 'Bundle FAILED the pinned comparison (see lines above)\n' >&2 + exit "$_rc" + fi + else + printf 'pinned comparison skipped (python3 not found) — HTTP/status only\n' + fi +fi diff --git a/jenner-check/t001_cmac2_counter/autoexec.sas b/jenner-check/t001_cmac2_counter/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t001_cmac2_counter/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t001_cmac2_counter/expected.json b/jenner-check/t001_cmac2_counter/expected.json new file mode 100644 index 0000000..2ef2f14 --- /dev/null +++ b/jenner-check/t001_cmac2_counter/expected.json @@ -0,0 +1,13 @@ +{ + "_captured_at": "2026-07-11T10:20:00Z", + "_captured_run_id": "r_39f17da9f3d54dd9a051c08fb8bea16c", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Wrote USSALES (10 rows, 3 columns).", + "NOTE: Macro variable COUNT_ORIG_USSALES = 10", + "NOTE: COUNT_ORIG_USSALES = 10" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t001_cmac2_counter/expected/log.txt b/jenner-check/t001_cmac2_counter/expected/log.txt new file mode 100644 index 0000000..9373ecd --- /dev/null +++ b/jenner-check/t001_cmac2_counter/expected/log.txt @@ -0,0 +1,23 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA USSALES + +NOTE: Processing inline DATALINES (10 lines) + +NOTE: Read 10 rows from DATALINES. +NOTE: Wrote USSALES (10 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC SQL + +NOTE: Macro variable COUNT_ORIG_USSALES = 10 +NOTE: PROC SQL statement used. +NOTE: COUNT_ORIG_USSALES = 10 +NOTE: DATA _NULL_ + +US sales observation count: 10 + +NOTE: Wrote _NULL_ (0 rows, 0 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds diff --git a/jenner-check/t001_cmac2_counter/expected/output.txt b/jenner-check/t001_cmac2_counter/expected/output.txt new file mode 100644 index 0000000..e69de29 diff --git a/jenner-check/t001_cmac2_counter/meta.json b/jenner-check/t001_cmac2_counter/meta.json new file mode 100644 index 0000000..c099680 --- /dev/null +++ b/jenner-check/t001_cmac2_counter/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t001_cmac2_counter", + "source_file": "common-macros.sas", + "source_blob_sha": "665695c2d94bc5c53b6fea3830a1e0c14b153a05", + "source_commit": "7c52a1a98dfdeca25575a0b8e05221e04d96002d", + "notes": "CMAC2_COUNTER (common-macros.sas, PART 2) counts rows in a dataset via PROC SQL INTO :macrovar, used throughout the antidumping programs to log observation counts at each pipeline stage (e.g. COMPANY.&USDATA). Reproduced verbatim; caller supplies a 10-row mock USSALES dataset in the same shape (importer, quantity, price) the real programs count." +} diff --git a/jenner-check/t001_cmac2_counter/script.sas b/jenner-check/t001_cmac2_counter/script.sas new file mode 100644 index 0000000..07d3168 --- /dev/null +++ b/jenner-check/t001_cmac2_counter/script.sas @@ -0,0 +1,45 @@ +/* jenner-check bundle: CMAC2_COUNTER from common-macros.sas (slothbear/antidumping) + Source: common-macros.sas, PART 2: "MACRO TO GET COUNTS OF THE DATASETS + THAT NEED BE TO REVIEWED". Reproduced verbatim; only the call site and the + input dataset below are new, standing in for the COMPANY.&USDATA sales + extract the antidumping programs count at each pipeline stage. */ + +%MACRO CMAC2_COUNTER (DATASET =, MVAR =); + %GLOBAL COUNT_&MVAR.; + PROC SQL NOPRINT; + SELECT COUNT(*) + INTO :COUNT_&MVAR. + FROM &DATASET.; + QUIT; +%MEND CMAC2_COUNTER ; + +/* Stand-in for COMPANY.&USDATA: a small U.S. sales extract with the shape + the antidumping margin-calculation programs expect (importer, quantity, + gross unit price). */ +DATA USSALES; + LENGTH US_IMPORTER $20; + INPUT US_IMPORTER $20. SALEQTY GROSSPRICE; + DATALINES; +ACME TRADING 1200 45.50 +ACME TRADING 800 46.10 +DELTA IMPORTS 450 52.75 +DELTA IMPORTS 600 51.90 +DELTA IMPORTS 300 53.20 +GLOBAL METALS INC 2200 38.40 +GLOBAL METALS INC 1750 39.00 +HARBOR STEEL CO 900 41.15 +HARBOR STEEL CO 1100 40.85 +PACIFIC RIM TRADERS 500 49.60 +; +RUN; + +/* Real call convention from the repo (see nme-margin-calculation.sas, + "GET USSALES COUNT FOR LOG REPORTING PURPOSES"): + %CMAC2_COUNTER (DATASET = COMPANY.&USDATA, MVAR = ORIG_USSALES); */ +%CMAC2_COUNTER (DATASET = USSALES, MVAR = ORIG_USSALES); + +%PUT NOTE: COUNT_ORIG_USSALES = &COUNT_ORIG_USSALES; + +DATA _NULL_; + PUT "US sales observation count: &COUNT_ORIG_USSALES"; +RUN; diff --git a/jenner-check/t002_define_sale_date/autoexec.sas b/jenner-check/t002_define_sale_date/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t002_define_sale_date/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t002_define_sale_date/expected.json b/jenner-check/t002_define_sale_date/expected.json new file mode 100644 index 0000000..14b7b13 --- /dev/null +++ b/jenner-check/t002_define_sale_date/expected.json @@ -0,0 +1,16 @@ +{ + "_captured_at": "2026-07-11T10:20:00Z", + "_captured_run_id": "r_afaf4b7473f64fe487261b866ad1c7d9", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Wrote USSALES (5 rows, 3 columns).", + "NOTE: PROC PRINT completed: 5 observations printed, 3 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "output_contains": [ + "ACME TRADING 01MAR2024 01MAR2024", + "HARBOR STEEL CO 15JUN2024 10JUN2024" + ], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t002_define_sale_date/expected/log.txt b/jenner-check/t002_define_sale_date/expected/log.txt new file mode 100644 index 0000000..0ecbe01 --- /dev/null +++ b/jenner-check/t002_define_sale_date/expected/log.txt @@ -0,0 +1,13 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA USSALES + +NOTE: Processing inline DATALINES (5 lines) + +NOTE: Read 5 rows from DATALINES. +NOTE: Wrote USSALES (5 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=USSALES + +NOTE: PROC PRINT completed: 5 observations printed, 3 variables diff --git a/jenner-check/t002_define_sale_date/expected/output.txt b/jenner-check/t002_define_sale_date/expected/output.txt new file mode 100644 index 0000000..050127c --- /dev/null +++ b/jenner-check/t002_define_sale_date/expected/output.txt @@ -0,0 +1,9 @@ + SALE DATE AFTER SELECTIVE EARLIER-DATE ADJUSTMENT + + US_IMPORTER SHIPDATU SALEDATE +ACME TRADING 01MAR2024 01MAR2024 +DELTA IMPORTS 22APR2024 22APR2024 +GLOBAL METALS INC 28APR2024 28APR2024 +HARBOR STEEL CO 15JUN2024 10JUN2024 +PACIFIC RIM TRADERS 20JUN2024 20JUN2024 + diff --git a/jenner-check/t002_define_sale_date/meta.json b/jenner-check/t002_define_sale_date/meta.json new file mode 100644 index 0000000..27d8bd4 --- /dev/null +++ b/jenner-check/t002_define_sale_date/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t002_define_sale_date", + "source_file": "common-macros.sas", + "source_blob_sha": "665695c2d94bc5c53b6fea3830a1e0c14b153a05", + "source_commit": "7c52a1a98dfdeca25575a0b8e05221e04d96002d", + "notes": "DEFINE_SALE_DATE (common-macros.sas, PART 5) selectively rewrites a sale date to an earlier reported date when it truly precedes it. Reproduced verbatim, called the same way the repo calls it for U.S. sales (SALEDATE=, DATEBEFORESALE=, EARLIERDATE=). Mock 5-row USSALES dataset exercises both branches: rows where the earlier ship date is genuinely earlier (date gets overwritten) and one row where the 'earlier' date is actually later (date is left unchanged), matching the macro's IF EARLIERDATE < SALEDATE guard." +} diff --git a/jenner-check/t002_define_sale_date/script.sas b/jenner-check/t002_define_sale_date/script.sas new file mode 100644 index 0000000..7157546 --- /dev/null +++ b/jenner-check/t002_define_sale_date/script.sas @@ -0,0 +1,46 @@ +/* jenner-check bundle: DEFINE_SALE_DATE from common-macros.sas (slothbear/antidumping) + Source: common-macros.sas, PART 5: "SELECTIVELY ADJUST SALE DATE BASED ON + AN EARLIER DATE VARIABLE". Reproduced verbatim; the DATA step below is a + caller built from the real invocation used throughout the repo (see + nme-margin-calculation.sas / me-margin-calculation.sas, where U.S. sales + are read in with: + %DEFINE_SALE_DATE (SALEDATE = &USSALEDATE, DATEBEFORESALE = &USDATEBEFORESALE, + EARLIERDATE = &USEARLIERDATE); + applied inside the DATA step that reads COMPANY.&USDATA). */ + +%MACRO DEFINE_SALE_DATE (SALEDATE =, DATEBEFORESALE =, EARLIERDATE =); + %IF &DATEBEFORESALE = YES %THEN + %DO; + &EARLIERDATE = FLOOR(&EARLIERDATE); /* Eliminates the time part of sale date */ + /* when defined as a datetime variable. */ + IF &EARLIERDATE < &SALEDATE THEN + &SALEDATE = &EARLIERDATE; + %END; +%MEND DEFINE_SALE_DATE; + +/* Stand-in for COMPANY.&USDATA: invoice date (SALEDATE) vs. an earlier + contract/shipment date (SHIPDATU) -- the same shape the antidumping + programs use to decide which date actually governs the sale. */ +DATA USSALES; + LENGTH US_IMPORTER $20; + INFORMAT SALEDATE SHIPDATU DATE9.; + FORMAT SALEDATE SHIPDATU DATE9.; + INPUT US_IMPORTER $20. SALEDATE SHIPDATU; + + /* Real call convention from the repo: adjust SALEDATE to the earlier + SHIPDATU whenever "sales before shipment" applies to this record. */ + %DEFINE_SALE_DATE (SALEDATE = SALEDATE, DATEBEFORESALE = YES, EARLIERDATE = SHIPDATU); + + DATALINES; +ACME TRADING 15MAR2024 01MAR2024 +DELTA IMPORTS 22APR2024 22APR2024 +GLOBAL METALS INC 05MAY2024 28APR2024 +HARBOR STEEL CO 10JUN2024 15JUN2024 +PACIFIC RIM TRADERS 01JUL2024 20JUN2024 +; +RUN; + +PROC PRINT DATA = USSALES NOOBS; + VAR US_IMPORTER SHIPDATU SALEDATE; + TITLE "SALE DATE AFTER SELECTIVE EARLIER-DATE ADJUSTMENT"; +RUN; diff --git a/jenner-check/t003_unique_connums/autoexec.sas b/jenner-check/t003_unique_connums/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t003_unique_connums/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t003_unique_connums/expected.json b/jenner-check/t003_unique_connums/expected.json new file mode 100644 index 0000000..11b8eb9 --- /dev/null +++ b/jenner-check/t003_unique_connums/expected.json @@ -0,0 +1,17 @@ +{ + "_captured_at": "2026-07-11T10:20:00Z", + "_captured_run_id": "r_8ab569ca04ea4956bf0ebca62c7c315a", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Wrote USSALES (6 rows, 3 columns).", + "NOTE: 2 duplicate observations were deleted.", + "NOTE: PROC PRINT completed: 2 observations printed, 3 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "output_contains": [ + "C002 PREMIUM POLISHED", + "C002 STANDARD POLISHED" + ], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t003_unique_connums/expected/log.txt b/jenner-check/t003_unique_connums/expected/log.txt new file mode 100644 index 0000000..0a11303 --- /dev/null +++ b/jenner-check/t003_unique_connums/expected/log.txt @@ -0,0 +1,31 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA USSALES + +NOTE: Processing inline DATALINES (6 lines) + +NOTE: Read 6 rows from DATALINES. +NOTE: Wrote USSALES (6 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC SORT data=USSALES + +NOTE: 2 duplicate observations were deleted. +NOTE: Read 6 rows from USSALES. +NOTE: Wrote CONNUM_CHECK (4 rows, 3 columns). +NOTE: PROC SORT statement used. +NOTE: DATA NOT_UNIQUE_CONNUMS UNIQUE_CONNUMS + +NOTE: Multi-output DATA step splits stream into: NOT_UNIQUE_CONNUMS UNIQUE_CONNUMS + +NOTE: Read 4 rows from CONNUM_CHECK. +NOTE: Wrote NOT_UNIQUE_CONNUMS (2 rows, 5 columns). +NOTE: Wrote UNIQUE_CONNUMS (0 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=NOT_UNIQUE_CONNUMS + +NOTE: PROC PRINT completed: 2 observations printed, 3 variables +NOTE: PROC PRINT data=UNIQUE_CONNUMS + diff --git a/jenner-check/t003_unique_connums/expected/output.txt b/jenner-check/t003_unique_connums/expected/output.txt new file mode 100644 index 0000000..dcdb8d4 --- /dev/null +++ b/jenner-check/t003_unique_connums/expected/output.txt @@ -0,0 +1,7 @@ + Obs CONNUM GRADE FINISH +----- ------ -------- -------- + 1 C002 PREMIUM POLISHED + 2 C002 STANDARD POLISHED + +No observations in dataset. + diff --git a/jenner-check/t003_unique_connums/meta.json b/jenner-check/t003_unique_connums/meta.json new file mode 100644 index 0000000..470c495 --- /dev/null +++ b/jenner-check/t003_unique_connums/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t003_unique_connums", + "source_file": "nme-margin-calculation.sas", + "source_blob_sha": "f89283cc50a70e588fbfdc7e1dd9bfb03bb397ff", + "source_commit": "7c52a1a98dfdeca25575a0b8e05221e04d96002d", + "notes": "UNIQUE_CONNUMS (nme-margin-calculation.sas) verifies each control number (CONNUM) maps to exactly one combination of physical characteristics, using PROC SORT NODUPKEY plus first./last. BY-group logic. Reproduced verbatim. Mock 6-row USSALES dataset deliberately reports CONNUM C002 with two different GRADE/FINISH combinations so the failure path (NOT_UNIQUE_CONNUMS populated, UNIQUE_CONNUMS empty) is genuinely exercised, not just the trivial all-pass case. Note: this run also surfaced a minor Jenner log-message observation (column count in the 'NOTE: Wrote ...' line for multi-output DATA steps with DROP=/KEEP= reports pre-drop/keep counts) -- confirmed cosmetic only (actual dataset shape, PROC CONTENTS, and PROC PRINT are all correct); not pinned in expected.json's log_contains and not filed as a regression test in this run (could not obtain a sas-expert BUG verdict -- no subagent dispatch tool available in this environment)." +} diff --git a/jenner-check/t003_unique_connums/script.sas b/jenner-check/t003_unique_connums/script.sas new file mode 100644 index 0000000..ec7678e --- /dev/null +++ b/jenner-check/t003_unique_connums/script.sas @@ -0,0 +1,73 @@ +/* jenner-check bundle: UNIQUE_CONNUMS from nme-margin-calculation.sas + (slothbear/antidumping) + Source: nme-margin-calculation.sas, "CONNUM UNIQUENESS TEST" -- checks + that each control number (CONNUM) in a sales dataset maps to exactly one + combination of physical characteristics, which is the model-matching + backbone the whole antidumping margin calculation depends on. + Reproduced verbatim; only the call site and its mock U.S. sales dataset + are new. */ + +%MACRO UNIQUE_CONNUMS (DATASET); + %IF %UPCASE(&CONNUMS) = YES %THEN + %DO; + PROC SORT DATA = &DATASET (KEEP = &USCONNUM &USPHVARS) + OUT = CONNUM_CHECK NODUPKEY; + BY &USCONNUM &USPHVARS; + RUN; + + DATA NOT_UNIQUE_CONNUMS (DROP = COUNT UNIQUE) + UNIQUE_CONNUMS (KEEP = UNIQUE); + SET CONNUM_CHECK END = EOF; + BY &USCONNUM; + IF NOT (FIRST.&USCONNUM AND LAST.&USCONNUM) THEN + DO; + COUNT + 1; + OUTPUT NOT_UNIQUE_CONNUMS; + END; + ELSE + IF EOF AND (COUNT = 0) THEN + DO; + UNIQUE = "CONNUMS IN THE &DATASET DATASET ARE UNIQUELY DEFINED"; + OUTPUT UNIQUE_CONNUMS; + END; + RUN; + + PROC PRINT DATA = NOT_UNIQUE_CONNUMS; + TITLE3 "CONNUM UNIQUENESS TEST"; + TITLE4 "CONNUMS IN THE &DATASET DATASET THAT ARE NOT UNIQUELY DEFINED"; + TITLE5 "CONNUMS HAVE MORE THAN ONE UNIQUE COMBINATION"; + TITLE6 "OF PHYSICAL CHARACTERISTICS ASSOCIATED WITH THEM"; + RUN; + + PROC PRINT DATA = UNIQUE_CONNUMS SPLIT = '*' NOOBS; + TITLE4; + LABEL UNIQUE = '00'x; + RUN; + %END; +%MEND UNIQUE_CONNUMS; + +/* Repo's real call-site setup (see nme-margin-calculation.sas, "PART 2: + GET U.S., FOP, AND SV DATA"): CONNUMS turns the test on/off, USCONNUM + names the control-number variable, USPHVARS lists the physical + characteristic variables that make up the model match. */ +%LET CONNUMS = YES; +%LET USCONNUM = CONNUM; +%LET USPHVARS = GRADE FINISH; + +/* Stand-in for COMPANY.&USDATA: a small U.S. sales extract where one + CONNUM (C002) is intentionally reported with two different GRADE/FINISH + combinations, so the uniqueness test has something real to catch. */ +DATA USSALES; + LENGTH CONNUM $4 GRADE $10 FINISH $10; + INPUT CONNUM $ GRADE $ FINISH $; + DATALINES; +C001 STANDARD POLISHED +C001 STANDARD POLISHED +C002 STANDARD POLISHED +C002 PREMIUM POLISHED +C003 PREMIUM MATTE +C003 PREMIUM MATTE +; +RUN; + +%UNIQUE_CONNUMS (USSALES); diff --git a/jenner-check/t004_create_quarters/autoexec.sas b/jenner-check/t004_create_quarters/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t004_create_quarters/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t004_create_quarters/expected.json b/jenner-check/t004_create_quarters/expected.json new file mode 100644 index 0000000..3787778 --- /dev/null +++ b/jenner-check/t004_create_quarters/expected.json @@ -0,0 +1,16 @@ +{ + "_captured_at": "2026-07-11T10:20:00Z", + "_captured_run_id": "r_3a0fa226a4c94e00b053532b0e090456", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Wrote USSALES (5 rows, 3 columns).", + "NOTE: PROC PRINT completed: 5 observations printed, 3 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "output_contains": [ + "ACME TRADING 15JAN2024 1", + "PACIFIC RIM TRADERS 19DEC2024 4" + ], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t004_create_quarters/expected/log.txt b/jenner-check/t004_create_quarters/expected/log.txt new file mode 100644 index 0000000..0ecbe01 --- /dev/null +++ b/jenner-check/t004_create_quarters/expected/log.txt @@ -0,0 +1,13 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA USSALES + +NOTE: Processing inline DATALINES (5 lines) + +NOTE: Read 5 rows from DATALINES. +NOTE: Wrote USSALES (5 rows, 3 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=USSALES + +NOTE: PROC PRINT completed: 5 observations printed, 3 variables diff --git a/jenner-check/t004_create_quarters/expected/output.txt b/jenner-check/t004_create_quarters/expected/output.txt new file mode 100644 index 0000000..23b65d0 --- /dev/null +++ b/jenner-check/t004_create_quarters/expected/output.txt @@ -0,0 +1,9 @@ + U.S. SALES ASSIGNED TO PERIOD-OF-REVIEW QUARTERS + + US_IMPORTER SALEDATE QTR +ACME TRADING 15JAN2024 1 +DELTA IMPORTS 20APR2024 2 +GLOBAL METALS INC 03JUL2024 3 +HARBOR STEEL CO 28SEP2024 3 +PACIFIC RIM TRADERS 19DEC2024 4 + diff --git a/jenner-check/t004_create_quarters/meta.json b/jenner-check/t004_create_quarters/meta.json new file mode 100644 index 0000000..0fb0fee --- /dev/null +++ b/jenner-check/t004_create_quarters/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t004_create_quarters", + "source_file": "me-macros.sas", + "source_blob_sha": "270d49c4d63c171da36b80a1224c14df99bbee04", + "source_commit": "7c52a1a98dfdeca25575a0b8e05221e04d96002d", + "notes": "CREATE_QUARTERS (me-macros.sas) assigns each sale to a period-of-review quarter relative to &BEGINPERIOD, used to group sales for time-based price comparisons. Reproduced verbatim, called the same way the repo calls it inside the DATA step that reads U.S. sales. Mock 5-row USSALES dataset spans all four quarters of a 2024 period of review; QTR assignment verified by hand against the macro's month-offset arithmetic." +} diff --git a/jenner-check/t004_create_quarters/script.sas b/jenner-check/t004_create_quarters/script.sas new file mode 100644 index 0000000..0e1042d --- /dev/null +++ b/jenner-check/t004_create_quarters/script.sas @@ -0,0 +1,71 @@ +/* jenner-check bundle: CREATE_QUARTERS from me-macros.sas (slothbear/antidumping) + Source: me-macros.sas, macro CREATE_QUARTERS -- assigns each U.S. sale to + a period-of-review quarter (QTR) relative to &BEGINPERIOD, which is how + the antidumping margin-calculation programs group sales for time-based + comparisons (see me-margin-calculation.sas / me-comparison-market.sas, + "Part 6: Create Concordance of Price-To-Price Matching Instructions"). + Reproduced verbatim; only the call site and its mock USSALES dataset + are new. */ + +%MACRO CREATE_QUARTERS(SLDT, PROGRAM); + %GLOBAL HM_TIME_PERIOD US_TIME_PERIOD; + + %LET HM_TIME_PERIOD = ; + %LET US_TIME_PERIOD = ; + + %IF %UPCASE(&COMPARE_BY_TIME) EQ YES %THEN + %DO; + %LET US_TIME_PERIOD = QTR; + + %IF &SALESDB = HMSALES %THEN + %DO; + %LET HM_TIME_PERIOD = QTR; + %END; + %ELSE + %IF &SALESDB = USSALES %THEN + %DO; + %LET HM_TIME_PERIOD = HM_TIME_PERIOD; + %END; + + FIRSTMONTH =(MONTH("&BEGINPERIOD"D)); + MTH = (MONTH(&SLDT) + (YEAR(&SLDT) - YEAR("&BEGINPERIOD"D)) * 12); + NQTR = (1 + (FLOOR((MTH - FIRSTMONTH) / 3))) ; + QTR = STRIP(PUT(NQTR, 2.)); + DROP FIRSTMONTH MTH NQTR; + %END; +%MEND CREATE_QUARTERS; + +/* Repo's real call-site setup (see me-margin-calculation.sas, Part 1-D + "PERIOD OF INVESTIGATION/REVIEW INFORMATION" and Part 6): COMPARE_BY_TIME + turns on time-based comparisons, BEGINPERIOD anchors quarter 1, SALESDB + names which sales dataset is currently being processed. */ +%LET COMPARE_BY_TIME = YES; +%LET BEGINPERIOD = 01JAN2024; +%LET SALESDB = USSALES; + +/* Stand-in for COMPANY.&USDATA: five U.S. sales spread across a + twelve-month period of review, the shape the antidumping programs + quarter-bucket for time-based comparisons. */ +DATA USSALES; + LENGTH US_IMPORTER $20; + INFORMAT SALEDATE DATE9.; + FORMAT SALEDATE DATE9.; + INPUT US_IMPORTER $20. SALEDATE; + + /* Real call convention from the repo (see me-margin-calculation.sas, + Part 6): %CREATE_QUARTERS (SLDT = &USSALEDATE, PROGRAM = MEMARG); */ + %CREATE_QUARTERS (SLDT = SALEDATE, PROGRAM = MEMARG); + + DATALINES; +ACME TRADING 15JAN2024 +DELTA IMPORTS 20APR2024 +GLOBAL METALS INC 03JUL2024 +HARBOR STEEL CO 28SEP2024 +PACIFIC RIM TRADERS 19DEC2024 +; +RUN; + +PROC PRINT DATA = USSALES NOOBS; + VAR US_IMPORTER SALEDATE QTR; + TITLE "U.S. SALES ASSIGNED TO PERIOD-OF-REVIEW QUARTERS"; +RUN; diff --git a/jenner-check/t005_convert_to_usd/autoexec.sas b/jenner-check/t005_convert_to_usd/autoexec.sas new file mode 100644 index 0000000..719f93f --- /dev/null +++ b/jenner-check/t005_convert_to_usd/autoexec.sas @@ -0,0 +1,2 @@ +/* cap input rows for the captured run */ +options obs=100; diff --git a/jenner-check/t005_convert_to_usd/expected.json b/jenner-check/t005_convert_to_usd/expected.json new file mode 100644 index 0000000..223b13b --- /dev/null +++ b/jenner-check/t005_convert_to_usd/expected.json @@ -0,0 +1,15 @@ +{ + "_captured_at": "2026-07-11T10:20:00Z", + "_captured_run_id": "r_f32a8f9ef63349d2b616d0c950d612c2", + "status": "ok", + "exit_code": 0, + "log_contains": [ + "NOTE: Wrote USSALES (5 rows, 4 columns).", + "NOTE: PROC PRINT completed: 5 observations printed, 4 variables" + ], + "log_does_not_contain": ["ERROR:", "[JENNER-ERROR"], + "output_contains": [ + "ACME TRADING 0.135 114.75 16.2" + ], + "diagnostics": {"parse_warnings": [], "runtime_warnings": []} +} diff --git a/jenner-check/t005_convert_to_usd/expected/log.txt b/jenner-check/t005_convert_to_usd/expected/log.txt new file mode 100644 index 0000000..e366955 --- /dev/null +++ b/jenner-check/t005_convert_to_usd/expected/log.txt @@ -0,0 +1,32 @@ +NOTE: Option OBS changed to 100. +NOTE: DATA USSALES + +NOTE: Processing inline DATALINES (5 lines) + +NOTE: Read 5 rows from DATALINES. +NOTE: Wrote USSALES (5 rows, 4 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: DATA USSALES + + +NOTE: Read 5 rows from USSALES. +NOTE: Wrote USSALES (5 rows, 7 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=USSALES + +NOTE: PROC PRINT completed: 5 observations printed, 5 variables +NOTE: DATA USSALES + + +NOTE: Read 5 rows from USSALES. +NOTE: Wrote USSALES (5 rows, 5 columns). +NOTE: DATA elapsed: + wall 0.00 seconds + cpu 0.00 seconds +NOTE: PROC PRINT data=USSALES + +NOTE: PROC PRINT completed: 5 observations printed, 4 variables diff --git a/jenner-check/t005_convert_to_usd/expected/output.txt b/jenner-check/t005_convert_to_usd/expected/output.txt new file mode 100644 index 0000000..273b132 --- /dev/null +++ b/jenner-check/t005_convert_to_usd/expected/output.txt @@ -0,0 +1,17 @@ + Obs EXRATE_DAILY FREIGHT BROKERAGE FREIGHT_USD BROKERAGE_USD +----- ------------ ------- --------- ----------- ------------- + 1 0.135 850 120 114.75 16.2 + 2 0.1362 620 95.5 84.444 13.0071 + 3 0.1341 990 140.25 132.759 18.807525 + 4 0.1358 410 60 55.678 8.148 + 5 0.1347 705 110.75 94.9635 14.918025 + + FREIGHT AND BROKERAGE CONVERTED TO U.S. DOLLARS + + US_IMPORTER EXRATE_DAILY FREIGHT_USD BROKERAGE_USD +ACME TRADING 0.135 114.75 16.2 +DELTA IMPORTS 0.1362 84.444 13.0071 +GLOBAL METALS INC 0.1341 132.759 18.807525 +HARBOR STEEL CO 0.1358 55.678 8.148 +PACIFIC RIM TRADERS 0.1347 94.9635 14.918025 + diff --git a/jenner-check/t005_convert_to_usd/meta.json b/jenner-check/t005_convert_to_usd/meta.json new file mode 100644 index 0000000..ef7e23f --- /dev/null +++ b/jenner-check/t005_convert_to_usd/meta.json @@ -0,0 +1,7 @@ +{ + "bundle": "t005_convert_to_usd", + "source_file": "nme-margin-calculation.sas", + "source_blob_sha": "f89283cc50a70e588fbfdc7e1dd9bfb03bb397ff", + "source_commit": "7c52a1a98dfdeca25575a0b8e05221e04d96002d", + "notes": "CONVERT_TO_USD (nme-margin-calculation.sas) converts foreign-currency sale adjustments (e.g. freight, brokerage) to U.S. dollars using each sale's own daily exchange rate, dynamically building the list of _USD output variable names via %SCAN/%DO %UNTIL. Reproduced verbatim, called the same way the repo calls it. Mock 5-row USSALES dataset carries per-row EXRATE_DAILY, FREIGHT, and BROKERAGE; conversion arithmetic verified by hand (FREIGHT_USD = FREIGHT * EXRATE_DAILY for every row) and DROP=&VARS_TO_USD confirmed to remove the original foreign-currency columns." +} diff --git a/jenner-check/t005_convert_to_usd/script.sas b/jenner-check/t005_convert_to_usd/script.sas new file mode 100644 index 0000000..2142a94 --- /dev/null +++ b/jenner-check/t005_convert_to_usd/script.sas @@ -0,0 +1,106 @@ +/* jenner-check bundle: CONVERT_TO_USD from nme-margin-calculation.sas + (slothbear/antidumping) + Source: nme-margin-calculation.sas, macro CONVERT_TO_USD -- converts + sale adjustments reported in a foreign currency (freight, brokerage, + etc.) into U.S. dollars using each sale's own daily exchange rate, then + drops the original foreign-currency columns. The macro builds its + "_USD" variable list dynamically with %SCAN/%DO %UNTIL over whatever + variable list is passed in -- reproduced verbatim below; only the call + site and its mock USSALES dataset are new. */ + +%MACRO CONVERT_TO_USD (USE_EXRATES = , EXDATA = , VARS_TO_USD =); + %IF %UPCASE(&USE_EXRATES) = YES %THEN + %DO; + DATA USSALES; + SET USSALES; + /*---------------------------------------------*/ + /* THE FOLLOWING ARRAY USES THE MACRO VARIABLE */ + /* VAR_TO_USD TO CONVERT ADJUSTMENTS EXPRESSED */ + /* IN FOREIGN CURRENCY TO U.S. DOLLARS. */ + /*---------------------------------------------*/ + + ARRAY CONVERT (*) &VARS_TO_USD; + + %LET I = 1; + + /*------------------------------------------------*/ + /* CREATE A LIST OF REVISED VARIABLES NAMES WITH */ + /* THE SUFFIX _USD. LOOP THROUGH THE VARIABLES IN */ + /* THE ORIGINAL LIST AND ADD THE REVISED VARIABLE */ + /* NAMES TO THE MACRO VARIABLE VARS_IN_USD. */ + /*------------------------------------------------*/ + + %LET VARS_IN_USD = ; + %DO %UNTIL (%SCAN(&VARS_TO_USD, &I, %STR( )) = %STR()); + %LET VARS_IN_USD = &VARS_IN_USD + %SYSFUNC(COMPRESS(%SCAN(&VARS_TO_USD, &I, %STR( )) _USD)); + %LET I = %EVAL(&I + 1); + %END; + %LET VARS_IN_USD = %CMPRES(&VARS_IN_USD); + + ARRAY CONVERTED (*) &VARS_IN_USD; + + /*-----------------------------------------------------*/ + /* CONVERT THE ORIGINAL VARIABLES IN THE ARRAY CONVERT */ + /* TO U.S DOLLARS USING THE DAILY EXCHANGE RATE AND */ + /* ASSIGN THE NEW VALUES TO NEW VARIABLES WITH THE */ + /* ORIGINAL NAME AND THE SUFFIX _USD THAT ARE IN THE */ + /* ARRAY CONVERTED. */ + /* */ + /* FOR EXAMPLE, IF THE VARIABLE COAL_SV IS DENOMINATED */ + /* IN A FOREIGN CURRENCY, THE VARIABLE COAL_SV_USD IS */ + /* CREATED AND DENOMINATED IN U.S. DOLLARS. */ + /*-----------------------------------------------------*/ + + DO I = 1 TO DIM(CONVERT); + CONVERTED(I) = CONVERT(I) * EXRATE_&EXDATA; + END; + RUN; + + PROC PRINT DATA = USSALES (OBS = &PRINTOBS); + VAR EXRATE_&EXDATA &VARS_TO_USD &VARS_IN_USD; + TITLE3 "VARIABLES CONVERTED TO U.S. DOLLARS"; + RUN; + + /*--------------------------------------------*/ + /* DROP THE ORIGINAL NON-CONVERTED VARIABLES. */ + /*--------------------------------------------*/ + + DATA USSALES; + SET USSALES (DROP = &VARS_TO_USD); + RUN; + %END; +%MEND CONVERT_TO_USD; + +/* Repo's real call-site setup (see nme-margin-calculation.sas, Part 6): + USE_EXRATES1 turns the conversion on, EXDATA1 names the exchange-rate + source (its column becomes EXRATE_), VARS_TO_USD1 lists the + foreign-currency adjustment columns to convert. PRINTOBS caps how many + rows the PROC PRINT preview shows, same as elsewhere in the repo. */ +%LET USE_EXRATES1 = YES; +%LET EXDATA1 = DAILY; +%LET VARS_TO_USD1 = FREIGHT BROKERAGE; +%LET PRINTOBS = 20; + +/* Stand-in for USSALES after PART 2/PART 6 of the real program: each row + already carries its own daily exchange rate (EXRATE_DAILY) merged in + from the exchange-rate dataset, plus freight and brokerage reported by + the foreign respondent in local currency. */ +DATA USSALES; + LENGTH US_IMPORTER $20; + INPUT US_IMPORTER $20. EXRATE_DAILY FREIGHT BROKERAGE; + DATALINES; +ACME TRADING 0.1350 850.00 120.00 +DELTA IMPORTS 0.1362 620.00 95.50 +GLOBAL METALS INC 0.1341 990.00 140.25 +HARBOR STEEL CO 0.1358 410.00 60.00 +PACIFIC RIM TRADERS 0.1347 705.00 110.75 +; +RUN; + +%CONVERT_TO_USD (USE_EXRATES = &USE_EXRATES1, EXDATA = &EXDATA1, VARS_TO_USD = &VARS_TO_USD1) + +PROC PRINT DATA = USSALES NOOBS; + VAR US_IMPORTER EXRATE_DAILY FREIGHT_USD BROKERAGE_USD; + TITLE "FREIGHT AND BROKERAGE CONVERTED TO U.S. DOLLARS"; +RUN;