diff --git a/.github/workflows/gitbook.yml b/.github/workflows/gitbook.yml new file mode 100644 index 0000000..00491af --- /dev/null +++ b/.github/workflows/gitbook.yml @@ -0,0 +1,115 @@ +name: GitBook + +# GitBook publishes this repo through Git sync (.gitbook.yaml -> SUMMARY.md), so +# whatever lands on main is what readers see — there is no build step in between +# to catch a stale table of contents or a link to a folder that was renamed. +# This workflow is that missing build step: +# +# * on pull requests -> validate only, so a bad TOC never reaches main +# * on push to main -> regenerate the navigation and commit any drift, which +# produces the push GitBook's Git sync reacts to +# +# Regenerating on main is what actually "updates GitBook": the commit this job +# makes is the trigger. No GitBook API token is involved. +# +# The bot commit does not re-run this workflow: GitHub suppresses workflow +# triggers for pushes made with the default GITHUB_TOKEN, so there is no loop. + +on: + push: + branches: [main] + paths: + - "Easy/**" + - "Medium/**" + - "Hard/**" + - "README.md" + - "SUMMARY.md" + - "_sidebar.md" + - ".gitbook.yaml" + - "tools/**" + - ".github/workflows/gitbook.yml" + pull_request: + paths: + - "Easy/**" + - "Medium/**" + - "Hard/**" + - "README.md" + - "SUMMARY.md" + - "_sidebar.md" + - ".gitbook.yaml" + - "tools/**" + - ".github/workflows/gitbook.yml" + workflow_dispatch: + +# Never let two runs race to commit regenerated navigation. +concurrency: + group: gitbook-${{ github.ref }} + cancel-in-progress: false + +jobs: + validate: + name: Validate GitBook sources + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check navigation, links, counts and math + run: ./tools/check-nav.sh + + - name: Explain how to fix + if: failure() + run: | + echo "::notice::Run './tools/gen-summary.sh && ./tools/gen-sidebar.sh && ./tools/mathfix.py' locally, then commit the result." + + publish: + name: Regenerate navigation and sync + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + # A real token is needed so the commit below is pushed as a normal + # commit that GitBook's Git sync app can see. + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Normalize math for GitBook + run: ./tools/mathfix.py + + - name: Regenerate SUMMARY.md and _sidebar.md + run: | + ./tools/gen-summary.sh + ./tools/gen-sidebar.sh + + - name: Commit drift, if any + id: commit + run: | + if git diff --quiet; then + echo "Navigation already up to date; nothing to push." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add SUMMARY.md _sidebar.md Easy Medium Hard + git commit -m "Regenerate GitBook navigation" + git push + echo "changed=true" >> "$GITHUB_OUTPUT" + + - name: Verify the state GitBook will sync + run: ./tools/check-nav.sh + + - name: Summary + run: | + { + echo "### GitBook sync" + echo + if [ "${{ steps.commit.outputs.changed }}" = "true" ]; then + echo "Navigation had drifted; regenerated and pushed a commit." + echo "GitBook Git sync picks this up on the new push." + else + echo "Navigation was already correct — GitBook syncs this commit as-is." + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/CLAUDE.md b/CLAUDE.md index f023e2a..f332105 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,14 @@ The repo is published two ways from the same markdown, each with its own generat Both scripts derive everything from the `/./solution*.md` layout, so correct folder naming matters. +To check the whole publishing surface before pushing: + +```bash +./tools/check-nav.sh # nav freshness, link resolution, README counts, math convention +``` + +CI runs the same script: `.github/workflows/gitbook.yml` validates it on pull requests and, on pushes to `main`, regenerates the navigation and commits any drift so GitBook's Git sync publishes correct pages. + ## Conventions ### Directory naming diff --git a/Easy/3622.Check-Divisibility-by-Digit-Sum-and-Product/description.md b/Easy/3622.Check-Divisibility-by-Digit-Sum-and-Product/description.md new file mode 100644 index 0000000..64e39a4 --- /dev/null +++ b/Easy/3622.Check-Divisibility-by-Digit-Sum-and-Product/description.md @@ -0,0 +1,33 @@ +# 3622. Check Divisibility by Digit Sum and Product + +You are given a positive integer `n`. Determine whether `n` is divisible by the +**sum** of the following two values: + +- The **digit sum** of `n` (the sum of its digits). +- The **digit product** of `n` (the product of its digits). + +Return `true` if `n` is divisible by this sum; otherwise, return `false`. + +## Example 1 + +```text +Input: n = 99 +Output: true +Explanation: +Since 99 is divisible by the sum (9 + 9 = 18) plus product (9 * 9 = 81) +of its digits (total 99), the output is true. +``` + +## Example 2 + +```text +Input: n = 23 +Output: false +Explanation: +Since 23 is not divisible by the sum (2 + 3 = 5) plus product (2 * 3 = 6) +of its digits (total 11), the output is false. +``` + +## Constraints + +- `1 <= n <= 10^6` diff --git a/Easy/3622.Check-Divisibility-by-Digit-Sum-and-Product/solution.md b/Easy/3622.Check-Divisibility-by-Digit-Sum-and-Product/solution.md new file mode 100644 index 0000000..b8c4574 --- /dev/null +++ b/Easy/3622.Check-Divisibility-by-Digit-Sum-and-Product/solution.md @@ -0,0 +1,127 @@ +# Intuition + +The check is a direct restatement of the definition: compute the digit sum $$S$$ and +the digit product $$P$$ of `n`, then test whether `n` is divisible by $$P + S$$. Both +quantities are folds over the same sequence of decimal digits, so a single scan that +updates two accumulators is enough — there is no need to store the digits. + +# Approach: Digit Extraction + +1. Initialize `product = 1` and `sum = 0`, and copy `n` into a working variable `temp`. +2. While `temp > 0`, take `digit = temp % 10`, multiply it into `product`, add it to + `sum`, then drop it with `temp /= 10`. +3. Return whether `n % (product + sum) == 0`. + +The peel relies on two standard identities: `temp % 10` is the last digit of `temp`, +and integer division `temp /= 10` removes it. The loop ends exactly when every digit +has been consumed. The seed values are the respective identities — `product = 1` +(seeding it with `0` would zero out every result) and `sum = 0`. Digits are visited +right-to-left, which is harmless because both addition and multiplication are +commutative. + +## Worked example: `n = 99` → `true` + +| iter | digit | sum | product | temp after | +| ---- | ----- | --- | ------- | ---------- | +| 1 | 9 | 9 | 9 | 9 | +| 2 | 9 | 18 | 81 | 0 | + +Divisor `= 81 + 18 = 99`, and `99 % 99 == 0`, so the answer is `true`. + +## Worked example: `n = 23` → `false` + +| iter | digit | sum | product | temp after | +| ---- | ----- | --- | ------- | ---------- | +| 1 | 3 | 3 | 3 | 2 | +| 2 | 2 | 5 | 6 | 0 | + +Divisor `= 6 + 5 = 11`, and `23 % 11 == 1`, so the answer is `false`. + +## Worked example: `n = 105` → `false` + +| iter | digit | sum | product | temp after | +| ---- | ----- | --- | ------- | ---------- | +| 1 | 5 | 5 | 5 | 10 | +| 2 | 0 | 5 | 0 | 1 | +| 3 | 1 | 6 | 0 | 0 | + +The zero digit collapses `product` to `0` permanently. Divisor `= 0 + 6 = 6`, and +`105 % 6 == 3`, so the answer is `false`. + +## Consequences worth noting + +- **A zero digit removes the product entirely.** For any `n` containing a `0`, the + test degenerates to "is `n` divisible by its digit sum?". +- **Digit sum `1` always returns `true`.** For `10`, `100`, ..., `1000000` the divisor + is `1`, and every integer is divisible by `1`. +- **No single-digit `n` ever returns `true`.** A lone digit `d` gives divisor + $$d + d = 2d$$, and $$d \bmod 2d = d \ne 0$$ for every $$d \ge 1$$. So `1`, `5`, + and `9` all return `false`. +- **The divisor is never zero.** Since $$n \ge 1$$ the leading digit is at least `1`, + hence $$S \ge 1$$; with $$P \ge 0$$ this gives $$P + S \ge 1$$ and the modulo is + always well defined. +- **No 32-bit overflow.** Under $$n \le 10^6$$ the digit product peaks at + $$9^6 = 531441$$ (at `999999`), far below the `i32` limit, so Rust's `i32` + arithmetic is safe without widening. + +# Complexity + +- Time complexity: $$O(\log_{10} n)$$ — one iteration per decimal digit, at most 7 + iterations for $$n \le 10^6$$. +- Space complexity: $$O(1)$$ — three scalar accumulators, no digit buffer. + +Converting `n` to a string and folding over its characters is equally correct but +allocates; the arithmetic peel avoids that allocation entirely. + +# Code + +## Go + +```go +func checkDivisibility(n int) bool { + temp := n + product, sum := 1, 0 + for temp > 0 { + digit := temp % 10 + product *= digit + sum += digit + temp /= 10 + } + return n % (product + sum) == 0 +} +``` + +## Rust + +```rust +impl Solution { + pub fn check_divisibility(n: i32) -> bool { + let (mut product, mut sum, mut temp) = (1, 0, n); + while temp > 0 { + let digit = temp % 10; + product *= digit; + sum += digit; + temp /= 10; + } + n % (product + sum) == 0 + } +} +``` + +# Test cases + +| `n` | product | sum | divisor | `n % divisor` | result | +| --------- | ------- | --- | ------- | ------------- | ------- | +| `99` | 81 | 18 | 99 | 0 | `true` | +| `23` | 6 | 5 | 11 | 1 | `false` | +| `1` | 1 | 1 | 2 | 1 | `false` | +| `9` | 9 | 9 | 18 | 9 | `false` | +| `10` | 0 | 1 | 1 | 0 | `true` | +| `20` | 0 | 2 | 2 | 0 | `true` | +| `12` | 2 | 3 | 5 | 2 | `false` | +| `36` | 18 | 9 | 27 | 9 | `false` | +| `105` | 0 | 6 | 6 | 3 | `false` | +| `1000000` | 0 | 1 | 1 | 0 | `true` | + +Sweeping the full constraint range, 54669 of the 1000000 values in +$$[1, 10^6]$$ return `true`. diff --git a/README.md b/README.md index cee3993..41a92dc 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ Easy/350.Intersection-of-Two-Arrays-II/ ## Solutions index -Total: **201** problems with at least one solution file. +Total: **202** problems with at least one solution file. Solution links use variant names when multiple approaches or languages exist (`main` = `solution.md`, others = `solution-<variant>.md`). -### Easy (51) +### Easy (52) | Problem | LeetCode | Solution | | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | @@ -74,6 +74,7 @@ Solution links use variant names when multiple approaches or languages exist (`m | 3375. Minimum Operations to Make Array Values Equal to k | [Link](https://leetcode.com/problems/minimum-operations-to-make-array-values-equal-to-k/) | [main](Easy/3375.Minimum-Operations-to-Make-Array-Values-Equal-to-k/solution.md) | | 3471. Find the Largest Almost Missing Integer | [Link](https://leetcode.com/problems/find-the-largest-almost-missing-integer/) | [main](Easy/3471.Find-the-Largest-Almost-Missing-Integer/solution.md) | | 3536. Maximum Product of Two Digits | [Link](https://leetcode.com/problems/maximum-product-of-two-digits/) | [main](Easy/3536.Maximum-Product-of-Two-Digits/solution.md) | +| 3622. Check Divisibility by Digit Sum and Product | [Link](https://leetcode.com/problems/check-divisibility-by-digit-sum-and-product/) | [main](Easy/3622.Check-Divisibility-by-Digit-Sum-and-Product/solution.md) | | 3637. Trionic Array I | [Link](https://leetcode.com/problems/trionic-array-i/) | [main](Easy/3637.Trionic-Array-I/solution.md) | | 3658. GCD of Odd and Even Sums | [Link](https://leetcode.com/problems/gcd-of-odd-and-even-sums/) | [main](Easy/3658.GCD-of-Odd-and-Even-Sums/solution.md) | | 3731. Find Missing Elements | [Link](https://leetcode.com/problems/find-missing-elements/) | [main](Easy/3731.Find-Missing-Elements/solution.md) | diff --git a/SUMMARY.md b/SUMMARY.md index 53a5dce..afe5898 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -56,6 +56,7 @@ * [3375. Minimum Operations to Make Array Values Equal to k](Easy/3375.Minimum-Operations-to-Make-Array-Values-Equal-to-k/solution.md) * [3471. Find the Largest Almost Missing Integer](Easy/3471.Find-the-Largest-Almost-Missing-Integer/solution.md) * [3536. Maximum Product of Two Digits](Easy/3536.Maximum-Product-of-Two-Digits/solution.md) +* [3622. Check Divisibility by Digit Sum and Product](Easy/3622.Check-Divisibility-by-Digit-Sum-and-Product/solution.md) * [3637. Trionic Array I](Easy/3637.Trionic-Array-I/solution.md) * [3658. GCD of Odd and Even Sums](Easy/3658.GCD-of-Odd-and-Even-Sums/solution.md) * [3731. Find Missing Elements](Easy/3731.Find-Missing-Elements/solution.md) diff --git a/_sidebar.md b/_sidebar.md index e123ee0..7f11b91 100644 --- a/_sidebar.md +++ b/_sidebar.md @@ -45,12 +45,18 @@ - 2582. Passing The Pillow - [rust](Easy/2582.Passing-The-Pillow/solution-rust.md) - [2696. Minimum String Length After Removing Substrings](Easy/2696.Minimum-String-Length-After-Removing-Substrings/solution.md) + - [2996. Smallest Missing Integer Greater Than Sequential Prefix Sum](Easy/2996.Smallest-Missing-Integer-Greater-Than-Sequential-Prefix-Sum/solution.md) + - [3069. Distribute Elements Into Two Arrays I](Easy/3069.Distribute-Elements-Into-Two-Arrays-I/solution.md) + - [3090. Maximum Length Substring With Two Occurrences](Easy/3090.Maximum-Length-Substring-With-Two-Occurrences/solution.md) - [3216. Lexicographically Smallest String After a Swap](Easy/3216.Lexicographically-Smallest-String-After-a-Swap/solution.md) - [3314. Construct the Minimum Bitwise Array I](Easy/3314.Construct-the-Minimum-Bitwise-Array-I/solution.md) - [3375. Minimum Operations to Make Array Values Equal to k](Easy/3375.Minimum-Operations-to-Make-Array-Values-Equal-to-k/solution.md) + - [3471. Find the Largest Almost Missing Integer](Easy/3471.Find-the-Largest-Almost-Missing-Integer/solution.md) - [3536. Maximum Product of Two Digits](Easy/3536.Maximum-Product-of-Two-Digits/solution.md) + - [3622. Check Divisibility by Digit Sum and Product](Easy/3622.Check-Divisibility-by-Digit-Sum-and-Product/solution.md) - [3637. Trionic Array I](Easy/3637.Trionic-Array-I/solution.md) - [3658. GCD of Odd and Even Sums](Easy/3658.GCD-of-Odd-and-Even-Sums/solution.md) + - [3731. Find Missing Elements](Easy/3731.Find-Missing-Elements/solution.md) - [3754. Concatenate Non Zero Digits and Multiply by Sum I](Easy/3754.Concatenate-Non-Zero-Digits-and-Multiply-by-Sum-I/solution.md) - Medium - [33. Search in rotated sorted array](Medium/33.Search-in-rotated-sorted-array/solution.md) @@ -87,6 +93,7 @@ - [853. Car Fleet](Medium/853.Car-Fleet/solution.md) - [874. Walking Robot Simulation](Medium/874.Walking-Robot-Simulation/solution.md) - [875. KoKo Eating Bananas](Medium/875.KoKo-Eating-Bananas/solution.md) + - [877. Stone Game](Medium/877.Stone-Game/solution.md) - [885. Spiral Matrix III](Medium/885.Spiral-Matrix-III/solution.md) - [921. Minimum Add To Make Parentheses Valid](Medium/921.Minimum-Add-To-Make-Parentheses-Valid/solution.md) - [947. Most Stones Removed with Same Row or Column](Medium/947.Most-Stones-Removed-with-Same-Row-or-Column/solution.md) @@ -105,6 +112,7 @@ - [1310. XOR Queries of a Subarray](Medium/1310.XOR-Queries-of-a-Subarray/solution.md) - [1371. Find the Longest Substring Containing Vowels in Even Counts](Medium/1371.Find-the-Longest-Substring-Containing-Vowels-in-Even-Counts/solution.md) - [1382. Balance A Binary Search Tree](Medium/1382.%20Balance-A-Binary-Search-Tree/solution.md) + - [1386. Cinema Seat Allocation](Medium/1386.Cinema-Seat-Allocation/solution.md) - [1395. Count Number of Teams](Medium/1395.Count-Number-of-Teams/solution.md) - [1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit](Medium/1438.Longest-Continuous-Subarray-With-Absolute-Diff-Less-Than-or-Equal-to-Limit/solution.md) - [1482. Minimum number of days to make m bouquets](Medium/1482.Minimum-number-of-days-to-make-m-bouquets/solution.md) @@ -161,6 +169,7 @@ - [2779. Maximum Beauty Of An Array After Applying Operation](Medium/2779.Maximum-Beauty-Of-An-Array-After-Applying-Operation/solution.md) - [2825. Make String a Subsequence Using Cyclic Increments](Medium/2825.Make-String-a-Subsequence-Using-Cyclic-Increments/solution.md) - [2924. Find Champion II](Medium/2924.Find-Champion-II/solution.md) + - [2958. Length of Longest Subarray With at Most K Frequency](Medium/2958.Length-of-Longest-Subarray-With-at-Most-K-Frequency/solution.md) - [3016. Minimum Number of Pushes to Type Word II](Medium/3016.Minimum-Number-of-Pushes-to-Type-Word-II/solution.md) - [3020. Find the Maximum Number of Elements in Subset](Medium/3020.Find-the-Maximum-Number-of-Elements-in-Subset/solution.md) - [3043. Find the Length of the Longest Common Prefix](Medium/3043.Find-the-Length-of-the-Longest-Common-Prefix/solution.md) @@ -169,11 +178,13 @@ - [3218. Minimum Cost for Cutting Cake I](Medium/3218.Minimum-Cost-for-Cutting-Cake-I/solution.md) - [3254. Find the Power of K Size Subarrays I](Medium/3254.Find-the-Power-of-K-Size-Subarrays-I/solution.md) - [3286. Find a Safe Walk Through a Grid](Medium/3286.Find-a-Safe-Walk-Through-a-Grid/solution.md) + - [3310. Remove Methods From Project](Medium/3310.Remove-Methods-From-Project/solution.md) - [3499. Maximize Active Section with Trade I](Medium/3499.Maximize-Active-Section-with-Trade-I/solution.md) - [3513. Number of Unique XOR Triplets I](Medium/3513.Number-of-Unique-XOR-Triplets-I/solution.md) - [3514. Number of Unique XOR Triplets II](Medium/3514.Number-of-Unique-XOR-Triplets-II/solution.md) - [3517. Smallest Palindromic Rearrangement I](Medium/3517.Smallest-Palindromic-Rearrangement-I/solution.md) - [3532. Path Existence Queries in a Graph I](Medium/3532.Path-Existence-Queries-in-a-Graph-I/solution.md) + - [3702. Longest Subsequence With Non Zero Bitwise XOR](Medium/3702.Longest-Subsequence-With-Non-Zero-Bitwise-XOR/solution.md) - [3756. Concatenate Non Zero Digits and Multiply by Sum II](Medium/3756.Concatenate-Non-Zero-Digits-and-Multiply-by-Sum-II/solution.md) - [3867. Sum of GCD of Formed Pairs](Medium/3867.Sum-of-GCD-of-Formed-Pairs/solution.md) - Hard @@ -195,6 +206,7 @@ - [995. Minimum Number of K Consecutive Bit Flips](Hard/995.Minimum-Number-of-K-Consecutive-Bit-Flips/solution.md) - [1106. Parsing A Boolean Expression](Hard/1106.Parsing-A-Boolean-Expression/solution.md) - [1301. Number of Paths with Max Score](Hard/1301.Number-of-Paths-with-Max-Score/solution.md) + - [1510. Stone Game IV](Hard/1510.Stone-Game-IV/solution.md) - [1579. Remove Max Number of Edges to Keep Graph Fully Traversable](Hard/1579.Remove-Max-Number-of-Edges-to-Keep-Graph-Fully-Traversable/solution.md) - [2071. Maximum Number of Tasks You Can Assign](Hard/2071.Maximum-Number-of-Tasks-You-Can-Assign/solution.md) - [2302. Count Subarrays With Score Less Than K](Hard/2302.Count-Subarrays-With-Score-Less-Than-K/solution.md) diff --git a/tools/check-nav.sh b/tools/check-nav.sh new file mode 100755 index 0000000..e52a042 --- /dev/null +++ b/tools/check-nav.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# +# Validate everything GitBook consumes before it syncs the repo. +# +# GitBook publishes this space via Git sync (.gitbook.yaml -> SUMMARY.md), so a +# stale or broken SUMMARY.md ships straight to the live site with no build step +# to catch it. This script is that build step. +# +# Checks: +# 1. SUMMARY.md and _sidebar.md match what the generators produce. +# 2. Every relative link in SUMMARY.md / _sidebar.md / README.md resolves. +# 3. README problem counts (total and per difficulty) match the folders on disk. +# 4. .gitbook.yaml points at files that exist. +# 5. No single-dollar math is left (GitBook only renders $$...$$). +# +# Run from anywhere: ./tools/check-nav.sh +# +set -uo pipefail + +cd "$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" + +fail=0 +ok() { printf 'ok %s\n' "$1"; } +bad() { printf 'FAIL %s\n' "$1"; fail=1; } + +# --- 1. generated navigation is up to date -------------------------------- +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +cp SUMMARY.md "$tmp/SUMMARY.md.orig" +cp _sidebar.md "$tmp/_sidebar.md.orig" + +./tools/gen-summary.sh >/dev/null +./tools/gen-sidebar.sh >/dev/null + +for f in SUMMARY.md _sidebar.md; do + if diff -q "$tmp/$f.orig" "$f" >/dev/null; then + ok "$f is up to date" + else + bad "$f is stale — run ./tools/gen-summary.sh && ./tools/gen-sidebar.sh" + diff -u "$tmp/$f.orig" "$f" | head -20 | sed 's/^/ /' + fi + # Restore the committed version so this script never mutates the tree. + cp "$tmp/$f.orig" "$f" +done + +# --- 2 & 3. link resolution and README counts ----------------------------- +# Done in Python: markdown targets here contain both escaped and bare +# parentheses (Medium/208.Implement-trie-(prefix-tree)) plus %20-encoded +# spaces, which a grep/sed pass cannot parse reliably. +python3 - <<'PY' +import pathlib +import re +import sys + +root = pathlib.Path.cwd() +problems = 0 + + +def link_targets(text): + """Yield each markdown link target, handling nested and escaped parens.""" + i = 0 + while (i := text.find("](", i)) != -1: + i += 2 + depth, out = 1, [] + while i < len(text): + ch = text[i] + if ch == "\\" and i + 1 < len(text): + out.append(text[i + 1]) + i += 2 + continue + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + break + elif ch == "\n": + break + out.append(ch) + i += 1 + yield "".join(out) + + +for name in ("SUMMARY.md", "_sidebar.md", "README.md"): + src = root / name + if not src.exists(): + print(f"FAIL {name} is missing") + problems += 1 + continue + + broken = [] + for target in link_targets(src.read_text(encoding="utf-8")): + target = target.split()[0] if target.split() else "" + if not target or target.startswith(("http://", "https://", "#", "mailto:")): + continue + target = target.split("#", 1)[0].replace("%20", " ") + if not (root / target).exists(): + broken.append(target) + + if broken: + print(f"FAIL {name} has {len(broken)} broken link(s)") + for t in broken[:10]: + print(f" -> {t}") + problems += 1 + else: + print(f"ok {name} links all resolve") + +# README counts vs folders on disk. +readme = (root / "README.md").read_text(encoding="utf-8") +total = 0 +for difficulty in ("Easy", "Medium", "Hard"): + on_disk = sum( + 1 + for d in (root / difficulty).iterdir() + if d.is_dir() and any(d.glob("solution*.md")) + ) + total += on_disk + + m = re.search(rf"^### {difficulty} \((\d+)\)", readme, re.M) + if not m: + print(f"FAIL README.md has no '### {difficulty} (N)' heading") + problems += 1 + elif int(m.group(1)) != on_disk: + print(f"FAIL README.md says '### {difficulty} ({m.group(1)})' but {on_disk} folder(s) exist") + problems += 1 + else: + print(f"ok README.md {difficulty} count ({on_disk}) matches disk") + +m = re.search(r"^Total: \*\*(\d+)\*\*", readme, re.M) +if not m: + print("FAIL README.md has no 'Total: **N** problems' line") + problems += 1 +elif int(m.group(1)) != total: + print(f"FAIL README.md says Total: **{m.group(1)}** but {total} problem folder(s) exist") + problems += 1 +else: + print(f"ok README.md total ({total}) matches disk") + +sys.exit(1 if problems else 0) +PY +[ $? -eq 0 ] || fail=1 + +# --- 4. .gitbook.yaml targets exist --------------------------------------- +if [ -f .gitbook.yaml ]; then + gb_ok=1 + for key in readme summary; do + target="$(sed -n "s/^ *$key: *//p" .gitbook.yaml | head -1)" + if [ -n "$target" ] && [ ! -f "$target" ]; then + printf ' .gitbook.yaml %s: %s does not exist\n' "$key" "$target" + gb_ok=0 + fi + done + if [ "$gb_ok" -eq 1 ]; then ok ".gitbook.yaml targets exist"; else bad ".gitbook.yaml points at missing file(s)"; fi +else + bad ".gitbook.yaml is missing — GitBook Git sync needs it" +fi + +# --- 5. math is GitBook-compatible ---------------------------------------- +if ./tools/mathfix.py --check >/dev/null 2>&1; then + ok 'all math uses $$...$$' +else + bad 'single-dollar math found — run ./tools/mathfix.py' + ./tools/mathfix.py --check 2>&1 | head -20 | sed 's/^/ /' +fi + +echo +if [ "$fail" -eq 0 ]; then + echo "All GitBook checks passed." +else + echo "GitBook checks failed." +fi +exit "$fail"