From 7ffb470db7eed1c54df5f731b6a4a3fc0023d769 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Mon, 31 Aug 2026 16:28:12 +0700 Subject: [PATCH] feat(go): rewrite in Go --- .gitignore | 8 ++- README.md | 57 ++++++++++++++++- go.mod | 3 + main.go | 35 ++++++++++ sieve.go | 58 +++++++++++++++++ sieve_test.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/Main.java | 35 ---------- 7 files changed, 332 insertions(+), 38 deletions(-) create mode 100644 go.mod create mode 100644 main.go create mode 100644 sieve.go create mode 100644 sieve_test.go delete mode 100644 src/Main.java diff --git a/.gitignore b/.gitignore index f63a24e..a231457 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .idea -*.iml -out + +# Built binary +/prime-generator + +# Generated output +primes.txt diff --git a/README.md b/README.md index 405150b..1a7b201 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,57 @@ # prime-generator -Generate prime numbers from 0 to 1_000_000_000 + +Generates every prime from 0 up to a limit with the sieve of Eratosthenes and +writes them one per line to a text file. The default limit is 1,000,000,000. + +*Originally written in Java. In 2026 it was rewritten in Go; the Java version is +on the [feature/java](https://github.com/tiennm99/prime-generator/tree/feature/java) +branch.* + +## Requirements + +- Go 1.25 or higher +- Disk space for the output — a full run to 1,000,000,000 writes 50,847,534 + primes, roughly 500 MB +- About 1 GB of RAM at the default limit, since the sieve holds one byte per + number + +## Usage + +```sh +go run . +``` + +Or build a binary first: + +```sh +go build -o prime-generator . +./prime-generator +``` + +### Flags + +| Flag | Default | Purpose | +| --- | --- | --- | +| `-n` | `1000000000` | Upper limit, inclusive, to sieve | +| `-out` | `primes.txt` | Output file path | + +```sh +# Primes below 100, written somewhere else +./prime-generator -n 100 -out small.txt +``` + +## Tests + +```sh +go test ./... +``` + +The sieve is checked against an independent trial-division implementation, and +against the published prime counts at powers of ten up to 1,000,000 — so a bug +in the sieve is not mirrored by the thing checking it. + +## Related + +[crawl-prime](https://github.com/tiennm99/crawl-prime) reaches the same output +file from the other direction: it downloads a published prime list instead of +computing one. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6d6c5c0 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/tiennm99/prime-generator + +go 1.25.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..534c69a --- /dev/null +++ b/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" +) + +const ( + defaultLimit = 1_000_000_000 + defaultOutput = "primes.txt" +) + +func main() { + limit := flag.Int("n", defaultLimit, "upper limit, inclusive, to sieve") + out := flag.String("out", defaultOutput, "output file path") + flag.Parse() + + f, err := os.Create(*out) + if err != nil { + log.Fatalf("create %s: %v", *out, err) + } + defer f.Close() + + count, err := writePrimes(f, sieve(*limit)) + if err != nil { + log.Fatalf("write %s: %v", *out, err) + } + if err := f.Close(); err != nil { + log.Fatalf("close %s: %v", *out, err) + } + + fmt.Printf("Wrote %d prime numbers up to %d to %s\n", count, *limit, *out) +} diff --git a/sieve.go b/sieve.go new file mode 100644 index 0000000..7b9ab7b --- /dev/null +++ b/sieve.go @@ -0,0 +1,58 @@ +package main + +import ( + "bufio" + "io" + "strconv" +) + +// sieve marks primality for every value in [0, limit] with the sieve of +// Eratosthenes. Index i holds true when i is prime, so the slice is indexed by +// the number itself and positions 0 and 1 stay false. +// +// A limit below 2 has no primes at all; the nil slice ranges as empty. +func sieve(limit int) []bool { + if limit < 2 { + return nil + } + + isPrime := make([]bool, limit+1) + for i := 2; i <= limit; i++ { + isPrime[i] = true + } + + // Composites below p*p already carry a smaller factor, so crossing out can + // start there, and a p past sqrt(limit) has nothing left to mark. + for p := 2; p*p <= limit; p++ { + if isPrime[p] { + for i := p * p; i <= limit; i += p { + isPrime[i] = false + } + } + } + + return isPrime +} + +// writePrimes writes one prime per line, ascending, and reports how many it +// wrote. The output is buffered because a full run emits tens of millions of +// lines. +func writePrimes(w io.Writer, isPrime []bool) (int, error) { + bw := bufio.NewWriter(w) + + count := 0 + for n, prime := range isPrime { + if !prime { + continue + } + if _, err := bw.WriteString(strconv.Itoa(n)); err != nil { + return count, err + } + if err := bw.WriteByte('\n'); err != nil { + return count, err + } + count++ + } + + return count, bw.Flush() +} diff --git a/sieve_test.go b/sieve_test.go new file mode 100644 index 0000000..09776d4 --- /dev/null +++ b/sieve_test.go @@ -0,0 +1,174 @@ +package main + +import ( + "errors" + "strconv" + "strings" + "testing" +) + +// primesByTrialDivision is a deliberately naive independent implementation, so +// a bug in the sieve is not mirrored by the thing checking it. +func primesByTrialDivision(limit int) []int { + var primes []int + for n := 2; n <= limit; n++ { + isPrime := true + for d := 2; d*d <= n; d++ { + if n%d == 0 { + isPrime = false + break + } + } + if isPrime { + primes = append(primes, n) + } + } + return primes +} + +func primesFrom(isPrime []bool) []int { + var primes []int + for n, prime := range isPrime { + if prime { + primes = append(primes, n) + } + } + return primes +} + +func TestSieveMatchesTrialDivision(t *testing.T) { + const limit = 5000 + + got := primesFrom(sieve(limit)) + want := primesByTrialDivision(limit) + + if len(got) != len(want) { + t.Fatalf("sieve(%d) produced %d primes, trial division produced %d", limit, len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("sieve(%d) primes[%d] = %d, want %d", limit, i, got[i], want[i]) + } + } +} + +func TestSieveKnownSmallPrimes(t *testing.T) { + want := []int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29} + + got := primesFrom(sieve(30)) + + if len(got) != len(want) { + t.Fatalf("sieve(30) = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("sieve(30)[%d] = %d, want %d", i, got[i], want[i]) + } + } +} + +// The prime-counting function at powers of ten is a published, independently +// known result, so it pins the sieve at a scale trial division cannot reach. +func TestSievePrimeCounts(t *testing.T) { + cases := []struct { + limit int + want int + }{ + {10, 4}, + {100, 25}, + {1_000, 168}, + {10_000, 1_229}, + {100_000, 9_592}, + {1_000_000, 78_498}, + } + + for _, tc := range cases { + t.Run(strconv.Itoa(tc.limit), func(t *testing.T) { + if got := len(primesFrom(sieve(tc.limit))); got != tc.want { + t.Errorf("pi(%d) = %d, want %d", tc.limit, got, tc.want) + } + }) + } +} + +func TestSieveBelowTwoHasNoPrimes(t *testing.T) { + for _, limit := range []int{-1, 0, 1} { + if got := primesFrom(sieve(limit)); len(got) != 0 { + t.Errorf("sieve(%d) = %v, want no primes", limit, got) + } + } +} + +func TestSieveExactlyTwo(t *testing.T) { + got := primesFrom(sieve(2)) + if len(got) != 1 || got[0] != 2 { + t.Errorf("sieve(2) = %v, want [2]", got) + } +} + +// 0 and 1 are not prime, and the slice is indexed by the number itself, so +// those positions must stay false rather than shifting every later index. +func TestSieveIndexingIsByNumber(t *testing.T) { + isPrime := sieve(10) + + if len(isPrime) != 11 { + t.Fatalf("len(sieve(10)) = %d, want 11", len(isPrime)) + } + if isPrime[0] || isPrime[1] { + t.Error("0 and 1 must not be marked prime") + } + if !isPrime[7] { + t.Error("7 must be marked prime") + } + if isPrime[9] { + t.Error("9 must not be marked prime") + } +} + +func TestWritePrimesFormat(t *testing.T) { + var buf strings.Builder + + count, err := writePrimes(&buf, sieve(20)) + if err != nil { + t.Fatalf("writePrimes returned error: %v", err) + } + + const want = "2\n3\n5\n7\n11\n13\n17\n19\n" + if buf.String() != want { + t.Errorf("output = %q, want %q", buf.String(), want) + } + if count != 8 { + t.Errorf("count = %d, want 8", count) + } +} + +func TestWritePrimesEmpty(t *testing.T) { + var buf strings.Builder + + count, err := writePrimes(&buf, sieve(1)) + if err != nil { + t.Fatalf("writePrimes returned error: %v", err) + } + if buf.String() != "" { + t.Errorf("output = %q, want empty", buf.String()) + } + if count != 0 { + t.Errorf("count = %d, want 0", count) + } +} + +type errWriter struct{ err error } + +func (w errWriter) Write([]byte) (int, error) { return 0, w.err } + +func TestWritePrimesPropagatesWriteError(t *testing.T) { + wantErr := errors.New("disk full") + + // Enough primes to overflow bufio's buffer, so the failure surfaces during + // writing rather than only at the final flush. + _, err := writePrimes(errWriter{err: wantErr}, sieve(200_000)) + + if !errors.Is(err, wantErr) { + t.Errorf("writePrimes error = %v, want %v", err, wantErr) + } +} diff --git a/src/Main.java b/src/Main.java deleted file mode 100644 index cbd59a9..0000000 --- a/src/Main.java +++ /dev/null @@ -1,35 +0,0 @@ -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; - -public class Main { - - public static void main(String[] args) { - int n = 1_000_000_000; // Upper limit - boolean[] isPrime = new boolean[n + 1]; - - for (int i = 2; i <= n; i++) { - isPrime[i] = true; - } - - for (int p = 2; p * p <= n; p++) { - if (isPrime[p]) { - for (int i = p * p; i <= n; i += p) { - isPrime[i] = false; - } - } - } - - try (BufferedWriter writer = new BufferedWriter(new FileWriter("primes.txt"))) { - for (int i = 2; i <= n; i++) { - if (isPrime[i]) { - writer.write(Integer.toString(i)); - writer.newLine(); - } - } - System.out.println("Prime numbers up to " + n + " have been saved to primes.txt."); - } catch (IOException e) { - e.printStackTrace(); - } - } -}