Skip to content

Commit dfaf901

Browse files
mjradwinclaude
andcommitted
Add Pirkei Avot Summer schedule
Port Pirkei Avot ("Ethics of the Fathers") from @hebcal/learning: one chapter studied on each Shabbat of the summer between Pesach and Rosh Hashana, the six-chapter cycle repeating with doubled chapters at the end of the season. Computed from the Hebrew calendar with no fixed start date; takes the il flag to select the Israel vs Diaspora schedule (8th day Pesach / 2nd day Shavuot skips) and skips Erev/Tish'a B'Av Shabbatot. Registered as "pirkeiAvotSummer"; events return event.DAILY_LEARNING. Tested against the full 5783 (IL + Diaspora) and 5784 sequences from the @hebcal/learning test suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4739fa4 commit dfaf901

7 files changed

Lines changed: 284 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ mirrors the relationship between `@hebcal/core` and `@hebcal/learning`:
3333
(~17-month cycle).
3434
- **psalms** — Daily Psalms (Tehillim) in the traditional 30-day cycle,
3535
completing the book of Psalms each Hebrew month.
36+
- **pirkeiavot** — Pirkei Avot ("Ethics of the Fathers"), one chapter on
37+
each Shabbat of the summer between Pesach and Rosh Hashana.
3638

3739
## Usage
3840

@@ -86,14 +88,15 @@ The schedules register the following case-insensitive names with the
8688
- `dafWeeklySunday`
8789
- `perekYomi`
8890
- `psalms`
91+
- `pirkeiAvotSummer`
8992

9093
## Roadmap
9194

9295
The goal is to eventually port all of the daily learning schedules from
9396
the TypeScript [@hebcal/learning](https://github.com/hebcal/hebcal-learning)
9497
package. Still to port: Arukh HaShulchan Yomi, Sefer HaMitzvot, Chofetz
95-
Chaim, Shemirat HaLashon, Pirkei Avot, Tanakh Yomi, Kitzur Shulchan
96-
Aruch, Dirshu Amud Yomi, etc.
98+
Chaim, Shemirat HaLashon, Tanakh Yomi, Kitzur Shulchan Aruch, Dirshu
99+
Amud Yomi, etc.
97100

98101
## License
99102

‎integration_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
func TestRegistry(t *testing.T) {
2727
assert := assert.New(t)
2828
assert.Equal(
29-
[]string{"929", "dafweekly", "dafweeklysunday", "dafyomi", "mishnayomi", "nachyomi", "perekyomi", "psalms", "rambam1", "rambam3", "yerushalmi-schottenstein", "yerushalmi-vilna"},
29+
[]string{"929", "dafweekly", "dafweeklysunday", "dafyomi", "mishnayomi", "nachyomi", "perekyomi", "pirkeiavotsummer", "psalms", "rambam1", "rambam3", "yerushalmi-schottenstein", "yerushalmi-vilna"},
3030
dailylearning.GetCalendars(),
3131
)
3232

‎learning.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
_ "github.com/hebcal/learning/nachyomi"
2222
_ "github.com/hebcal/learning/nine29"
2323
_ "github.com/hebcal/learning/perekyomi"
24+
_ "github.com/hebcal/learning/pirkeiavot"
2425
_ "github.com/hebcal/learning/psalms"
2526
_ "github.com/hebcal/learning/rambam"
2627
_ "github.com/hebcal/learning/yerushalmi"

‎pirkeiavot/event.go‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package pirkeiavot
2+
3+
// Hebcal - A Jewish Calendar Generator
4+
// Copyright (c) 2026 Michael J. Radwin
5+
//
6+
// This program is free software; you can redistribute it and/or
7+
// modify it under the terms of the GNU General Public License
8+
// as published by the Free Software Foundation; either version 2
9+
// of the License, or (at your option) any later version.
10+
//
11+
// This program is distributed in the hope that it will be useful,
12+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
// GNU General Public License for more details.
15+
//
16+
// You should have received a copy of the GNU General Public License
17+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
19+
import (
20+
"strconv"
21+
"strings"
22+
23+
"github.com/hebcal/gematriya"
24+
"github.com/hebcal/hdate"
25+
"github.com/hebcal/hebcal-go/event"
26+
"github.com/hebcal/hebcal-go/locales"
27+
)
28+
29+
const pirkeiAvot = "Pirkei Avot"
30+
31+
type pirkeiAvotEvent struct {
32+
Date hdate.HDate
33+
Chapters []int
34+
}
35+
36+
// NewPirkeiAvotSummerEvent returns a calendar event for the Pirkei Avot
37+
// chapter(s) studied on the given Shabbat.
38+
func NewPirkeiAvotSummerEvent(hd hdate.HDate, chapters []int) event.CalEvent {
39+
return pirkeiAvotEvent{Date: hd, Chapters: chapters}
40+
}
41+
42+
func (ev pirkeiAvotEvent) GetDate() hdate.HDate {
43+
return ev.Date
44+
}
45+
46+
func (ev pirkeiAvotEvent) Render(locale string) string {
47+
book, _ := locales.LookupTranslation(pirkeiAvot, locale)
48+
hebrew := false
49+
if l := strings.ToLower(locale); l == "he" || l == "he-x-nonikud" {
50+
hebrew = true
51+
}
52+
parts := make([]string, len(ev.Chapters))
53+
for i, c := range ev.Chapters {
54+
if hebrew {
55+
parts[i] = gematriya.Gematriya(c)
56+
} else {
57+
parts[i] = strconv.Itoa(c)
58+
}
59+
}
60+
return book + " " + strings.Join(parts, "-")
61+
}
62+
63+
func (ev pirkeiAvotEvent) GetFlags() event.HolidayFlags {
64+
return event.DAILY_LEARNING
65+
}
66+
67+
func (ev pirkeiAvotEvent) GetEmoji() string {
68+
return ""
69+
}
70+
71+
func (ev pirkeiAvotEvent) Basename() string {
72+
parts := make([]string, len(ev.Chapters))
73+
for i, c := range ev.Chapters {
74+
parts[i] = strconv.Itoa(c)
75+
}
76+
return pirkeiAvot + " " + strings.Join(parts, "-")
77+
}

‎pirkeiavot/pirkeiavot.go‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Hebcal's pirkeiavot package calculates the Pirkei Avot ("Ethics of the
2+
// Fathers") summer schedule: one chapter studied on each Shabbat between
3+
// Pesach and Rosh Hashana, the six-chapter cycle repeating with doubled
4+
// chapters at the end to fill the season.
5+
//
6+
// https://en.wikipedia.org/wiki/Pirkei_Avot#Study_of_the_work
7+
package pirkeiavot
8+
9+
// Hebcal - A Jewish Calendar Generator
10+
// Copyright (c) 2026 Michael J. Radwin
11+
//
12+
// This program is free software; you can redistribute it and/or
13+
// modify it under the terms of the GNU General Public License
14+
// as published by the Free Software Foundation; either version 2
15+
// of the License, or (at your option) any later version.
16+
//
17+
// This program is distributed in the hope that it will be useful,
18+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
// GNU General Public License for more details.
21+
//
22+
// You should have received a copy of the GNU General Public License
23+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
24+
25+
import (
26+
"time"
27+
28+
"github.com/hebcal/hdate"
29+
)
30+
31+
// New returns the Pirkei Avot chapter(s) studied on the given Shabbat.
32+
//
33+
// The il argument selects the Israel schedule (no 8th day Pesach, no 2nd
34+
// day Shavuot) when true, or the Diaspora schedule when false.
35+
//
36+
// The second return value is false when the date is not a Pirkei Avot
37+
// Shabbat: not a Saturday, before Pesach, after Rosh Hashana, or a
38+
// Shabbat coinciding with a holiday on which Pirkei Avot is skipped (8th
39+
// day Pesach / 2nd day Shavuot in the Diaspora; Erev Tish'a B'Av or
40+
// Tish'a B'Av).
41+
func New(hd hdate.HDate, il bool) ([]int, bool) {
42+
if hd.Weekday() != time.Saturday {
43+
return nil, false
44+
}
45+
hyear := hd.Year()
46+
pesach7 := hdate.New(hyear, hdate.Nisan, 21)
47+
if hd.Abs() <= pesach7.Abs() {
48+
return nil, false // before Pesach
49+
}
50+
first := pesach7.After(time.Saturday)
51+
weekDiff := ceilDiv(hd.Abs()-first.Abs(), 7)
52+
53+
// When a holiday falls on Shabbat, Pirkei Avot is not studied; the same
54+
// applies to Erev Tish'a B'Av.
55+
var holidays []hdate.HDate
56+
if !il {
57+
// 8th day Pesach and 2nd day Shavuot are observed only in the Diaspora.
58+
holidays = append(holidays, pesach7.Next(), hdate.New(hyear, hdate.Sivan, 7))
59+
}
60+
av8 := hdate.New(hyear, hdate.Av, 8)
61+
holidays = append(holidays, av8, av8.Next()) // Erev 9 Av and Tish'a B'Av
62+
63+
for _, day := range holidays {
64+
if day.Abs() == hd.Abs() {
65+
return nil, false
66+
}
67+
if day.Abs()-hd.Abs() <= 0 && day.Weekday() == time.Saturday {
68+
weekDiff-- // a holiday Shabbat already passed; don't count that week
69+
}
70+
}
71+
72+
if weekDiff < 0 {
73+
return nil, false
74+
} else if weekDiff < 18 {
75+
return []int{int(weekDiff%6) + 1}, true
76+
}
77+
78+
// The final round doubles up chapters to reach Rosh Hashana.
79+
rh := hdate.New(hyear+1, hdate.Tishrei, 1)
80+
last := rh.Before(time.Saturday)
81+
weeksRemain := ceilDiv(last.Abs()-hd.Abs(), 7)
82+
switch weeksRemain {
83+
case 0:
84+
return []int{5, 6}, true
85+
case 1:
86+
return []int{3, 4}, true
87+
case 2:
88+
if weekDiff%6 == 1 {
89+
return []int{2}, true
90+
}
91+
return []int{1, 2}, true
92+
case 3:
93+
return []int{1}, true
94+
}
95+
return nil, false
96+
}
97+
98+
// ceilDiv returns ceil(a/b) for b > 0, matching JavaScript Math.ceil.
99+
func ceilDiv(a, b int64) int64 {
100+
if a >= 0 {
101+
return (a + b - 1) / b
102+
}
103+
return -((-a) / b)
104+
}

‎pirkeiavot/pirkeiavot_test.go‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package pirkeiavot_test
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/hebcal/hdate"
8+
"github.com/hebcal/learning/pirkeiavot"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
// collect returns the Pirkei Avot readings for every Shabbat from the
13+
// first Saturday on or after 21 Nisan through Erev Rosh Hashana, mirroring
14+
// the makeSaturdays helper in the @hebcal/learning test suite. A nil entry
15+
// represents a skipped Shabbat.
16+
func collect(year int, il bool) [][]int {
17+
pesach7 := hdate.New(year, hdate.Nisan, 21)
18+
fs := pesach7.OnOrAfter(time.Saturday)
19+
firstSat := fs.Abs()
20+
rh := hdate.New(year+1, hdate.Tishrei, 1)
21+
erevRH := rh.Abs() - 1
22+
var out [][]int
23+
for abs := firstSat; abs <= erevRH; abs += 7 {
24+
r, ok := pirkeiavot.New(hdate.FromRD(abs), il)
25+
if !ok {
26+
out = append(out, nil)
27+
} else {
28+
out = append(out, r)
29+
}
30+
}
31+
return out
32+
}
33+
34+
func TestPirkeiAvot5784Israel(t *testing.T) {
35+
assert.Equal(t, [][]int{
36+
{1}, {2}, {3}, {4}, {5}, {6},
37+
{1}, {2}, {3}, {4}, {5}, {6},
38+
{1}, {2}, {3}, {4}, {5}, {6},
39+
{1}, {2}, {3, 4}, {5, 6},
40+
}, collect(5784, true))
41+
}
42+
43+
func TestPirkeiAvot5783Diaspora(t *testing.T) {
44+
assert.Equal(t, [][]int{
45+
{1}, {2}, {3}, {4}, {5}, {6},
46+
nil, // 7 Sivan: 2nd day Shavuot
47+
{1}, {2}, {3}, {4}, {5}, {6},
48+
{1}, {2}, {3}, {4}, {5}, {6},
49+
{1, 2}, {3, 4}, {5, 6},
50+
}, collect(5783, false))
51+
}
52+
53+
func TestPirkeiAvot5783Israel(t *testing.T) {
54+
assert.Equal(t, [][]int{
55+
{1}, {2}, {3}, {4}, {5}, {6},
56+
{1}, {2}, {3}, {4}, {5}, {6},
57+
{1}, {2}, {3}, {4}, {5}, {6},
58+
{1}, {2}, {3, 4}, {5, 6},
59+
}, collect(5783, true))
60+
}
61+
62+
func TestPirkeiAvotNotShabbat(t *testing.T) {
63+
// 24 Nisan 3759 is not a Pirkei Avot Shabbat.
64+
_, ok := pirkeiavot.New(hdate.New(3759, hdate.Nisan, 24), true)
65+
assert.False(t, ok)
66+
}
67+
68+
func TestPirkeiAvotRender(t *testing.T) {
69+
ev2 := pirkeiavot.NewPirkeiAvotSummerEvent(hdate.New(5783, hdate.Elul, 9), []int{2})
70+
ev34 := pirkeiavot.NewPirkeiAvotSummerEvent(hdate.New(5783, hdate.Elul, 16), []int{3, 4})
71+
assert.Equal(t, "Pirkei Avot 2", ev2.Render("en"))
72+
assert.Equal(t, "Pirkei Avot 3-4", ev34.Render("en"))
73+
assert.Equal(t, "Pirkei Avot 3-4", ev34.Basename())
74+
assert.Equal(t, "פִּרְקֵי אָבוֹת ב׳", ev2.Render("he"))
75+
assert.Equal(t, "פִּרְקֵי אָבוֹת ג׳-ד׳", ev34.Render("he"))
76+
}

‎pirkeiavot/register.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package pirkeiavot
2+
3+
import (
4+
"github.com/hebcal/hdate"
5+
"github.com/hebcal/hebcal-go/dailylearning"
6+
"github.com/hebcal/hebcal-go/event"
7+
)
8+
9+
// Register the Pirkei Avot summer schedule with the dailylearning registry
10+
// under the name "pirkeiAvotSummer". It is purely computed from the Hebrew
11+
// calendar with no fixed start date, so no startDate is supplied.
12+
func init() {
13+
dailylearning.AddCalendar("pirkeiAvotSummer", func(hd hdate.HDate, il bool) event.CalEvent {
14+
chapters, ok := New(hd, il)
15+
if !ok {
16+
return nil
17+
}
18+
return NewPirkeiAvotSummerEvent(hd, chapters)
19+
})
20+
}

0 commit comments

Comments
 (0)