Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ jobs:
sudo apt-get update
sudo apt-get install -y shellcheck bats

- name: Shellcheck install.sh
run: shellcheck install.sh
- name: Shellcheck shell scripts
run: shellcheck ./*.sh

- name: Run install.sh test suite
run: bats tests/install.bats
- name: Run test suite
run: bats tests/*.bats
58 changes: 58 additions & 0 deletions moon.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env sh
# moon.sh β€” print the current phase of the Moon. A tiny, dependency-free
# flourish for agent-ready; deliberately NOT wired into install.sh.
#
# ./moon.sh [EPOCH_SECONDS]
#
# With no argument it uses the current time. Pass a Unix timestamp (whole
# seconds since 1970-01-01 UTC) to ask about any other moment β€” which is what
# makes it testable and lets you check a date that isn't today.

set -eu

# Reference new moon: 2000-01-06 18:14 UTC, as Unix epoch seconds.
KNOWN_NEW_MOON=947182440
# One synodic month (new moon to new moon): 29.53058867 days, in seconds.
SYNODIC=2551443

case "${1:-}" in
-h|--help) echo "usage: $0 [EPOCH_SECONDS]"; exit 0 ;;
-*) echo "unknown option: $1" >&2; exit 2 ;;
esac

# `date +%s` is a near-universal extension (GNU and BSD alike); there is no
# pure-POSIX way to read the current epoch. An explicit argument sidesteps it.
now="${1:-$(date +%s)}"

# Reject anything non-numeric up front so the arithmetic below can't misbehave.
case "$now" in
'' | *[!0-9]*)
echo "error: EPOCH_SECONDS must be a non-negative integer: '$now'" >&2
exit 2
;;
esac

# Seconds elapsed into the current lunar cycle (0 .. SYNODIC-1). The extra
# +SYNODIC guards against a reference-preceding timestamp yielding a negative
# remainder before we normalise it.
elapsed=$(( ( (now - KNOWN_NEW_MOON) % SYNODIC + SYNODIC ) % SYNODIC ))

# Age of the Moon in whole days since the last new moon.
age=$(( elapsed / 86400 ))

# Snap to one of eight named phases. The +SYNODIC/2 rounds to the nearest
# segment centre, so a full moon lands on "Full Moon" rather than just past it.
phase=$(( (elapsed * 8 + SYNODIC / 2) / SYNODIC % 8 ))

case "$phase" in
0) emoji="πŸŒ‘"; name="New Moon" ;;
1) emoji="πŸŒ’"; name="Waxing Crescent" ;;
2) emoji="πŸŒ“"; name="First Quarter" ;;
3) emoji="πŸŒ”"; name="Waxing Gibbous" ;;
4) emoji="πŸŒ•"; name="Full Moon" ;;
5) emoji="πŸŒ–"; name="Waning Gibbous" ;;
6) emoji="πŸŒ—"; name="Last Quarter" ;;
7) emoji="🌘"; name="Waning Crescent" ;;
esac

printf '%s %s (day %d of ~29.5)\n' "$emoji" "$name" "$age"
89 changes: 89 additions & 0 deletions tests/moon.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bats
# Exercises moon.sh against fixed timestamps so every phase is deterministic.
# Run with: bats tests/moon.bats
#
# The offsets below are measured from the script's reference new moon
# (2000-01-06 18:14 UTC = epoch 947182440), one per eighth of the lunar cycle.

setup() {
KIT="$(cd "$BATS_TEST_DIRNAME/.." && pwd)"
MOON="$KIT/moon.sh"
}

@test "reference instant reads as a new moon" {
run "$MOON" 947182440
[ "$status" -eq 0 ]
[[ "$output" == *"πŸŒ‘"* ]]
[[ "$output" == *"New Moon"* ]]
}

@test "quarter into the cycle reads as first quarter" {
run "$MOON" 947820300
[ "$status" -eq 0 ]
[[ "$output" == *"πŸŒ“"* ]]
[[ "$output" == *"First Quarter"* ]]
}

@test "half into the cycle reads as a full moon" {
run "$MOON" 948458161
[ "$status" -eq 0 ]
[[ "$output" == *"πŸŒ•"* ]]
[[ "$output" == *"Full Moon"* ]]
}

@test "past full but before last quarter reads as waning gibbous" {
run "$MOON" 948777083
[ "$status" -eq 0 ]
[[ "$output" == *"πŸŒ–"* ]]
[[ "$output" == *"Waning Gibbous"* ]]
}

@test "three quarters into the cycle reads as last quarter" {
run "$MOON" 949096022
[ "$status" -eq 0 ]
[[ "$output" == *"πŸŒ—"* ]]
[[ "$output" == *"Last Quarter"* ]]
}

@test "reports the moon's age in whole days" {
run "$MOON" 948458161
[ "$status" -eq 0 ]
[[ "$output" == *"day 14 of ~29.5"* ]]
}

@test "no argument uses the current time and still succeeds" {
run "$MOON"
[ "$status" -eq 0 ]
[[ "$output" == *"of ~29.5"* ]]
}

@test "a timestamp before the reference new moon does not crash" {
run "$MOON" 0
[ "$status" -eq 0 ]
[[ "$output" == *"of ~29.5"* ]]
}

@test "rejects a non-numeric timestamp" {
run "$MOON" abc
[ "$status" -eq 2 ]
[[ "$output" == *"non-negative integer"* ]]
}

@test "rejects an unknown option" {
run "$MOON" --nope
[ "$status" -eq 2 ]
[[ "$output" == *"unknown option"* ]]
}

@test "--help prints usage and exits 0" {
run "$MOON" --help
[ "$status" -eq 0 ]
[[ "$output" == *"usage:"* ]]
}

@test "works under a strict POSIX sh (dash), not just bash" {
command -v dash >/dev/null 2>&1 || skip "dash not installed"
run dash "$MOON" 948458161
[ "$status" -eq 0 ]
[[ "$output" == *"Full Moon"* ]]
}
Loading