nerdyversary

Nerdyversary
Small project about finding "nerdy anniversaries". An obvious example would be that after 3.1415... years one could celebrate the $\pi$-th anniversary. The code in this repo finds nice combinations of numbers like $\pi$, $e$, $\phi$ and so on and can construct fractions for the approximation.
Installation
From PyPI
Simply use
$ pip install nerdyversary
to install this package in your environment.
From Source
To install nerdyversary from source, follow these steps below:
Clone the repository
$ git clone https://github.com/rmnldwg/nerdyversaryCreate a Virtual Environment (optional, but recommended)
$ python3 -m venv .venvYou should do this with an installation of Python 3.10 or later. And don't forget to activate the environment with
$ source .venv/bin/activateUse
pipto install$ pip install -U pip setuptools setuptools-scm $ pip install .
Usage
Script
usage: nerdyversary [-h] [-v] [-d SPECIAL_DAY] [-s START] [-e END]
[--max-power MAX_POWER] [--factor-lim FACTOR_LIM]
[--format FORMAT]
Find beautiful nerdyversaries.
options:
-h, --help show this help message and exit
-v, --version Show the installed version and exit.
-d SPECIAL_DAY, --special-day SPECIAL_DAY
Date of the special day in ISO format. (default: 2023-01-04)
-s START, --start START
Date when to start with search in ISO format. (default:
2023-01-04)
-e END, --end END Date when to end the search in ISO format. (default:
2024-01-04)
--max-power MAX_POWER
Largest exponent to consider for building the nerdyversaries.
(default: 5)
--factor-lim FACTOR_LIM
Largest multiple of a symbol that is accepted. (default: 10)
--format FORMAT The output format that will be used by the `tabulate` package.
(default: simple)
The FORMAT argument must be one of the strings the tabulate package understands.
An example: The input
$ nerdyversary -d 2012-12-21 -s 2023-01-01 -e 2024-01-01 --format pipe --factor-lim 4 --max-power 3
will yield a markdown table that renders into the following:
| Date | Days | Years | Expression |
|---|---|---|---|
| 6. Jan 2023 | 3668 | 10.04 | $\frac{e^{3}}{2}$ |
| 24. Feb 2023 | 3717 | 10.18 | $\frac{5 e^{3}}{\pi^{2}}$ |
| 12. Jun 2023 | 3825 | 10.47 | $4 \phi^{2}$ |
| 19. Jun 2023 | 3832 | 10.49 | $\frac{5 \pi^{3}}{2 e^{2}}$ |
| 25. Jul 2023 | 3868 | 10.59 | $\frac{5 \phi^{3}}{2}$ |
| 5. Nov 2023 | 3971 | 10.87 | $4 e$ |
| 12. Nov 2023 | 3978 | 10.89 | $\frac{3 \pi^{2}}{e}$ |
| 14. Dec 2023 | 4010 | 10.98 | $\frac{3 \pi^{3}}{2 \phi^{3}}$ |
The symbols here are
- the golden ratio $\phi \approx 1.618\ldots$
- the number $\pi \approx 3.1415\ldots$
- Euler's number $e \approx 2.718\ldots$
When using this package as a library, arbitrary constants may be defined as symbols.
Library API
The API documentation is hosted here.
1""" 2.. include:: ../README.md 3""" 4import argparse 5import datetime as dt 6 7import numpy as np 8from sympy import Expr, NumberSymbol, S, latex 9from tabulate import tabulate 10 11from ._version import version 12 13TODAY = dt.date.today() 14DAYS_PER_YEAR = 365.2425 15SYMBOLS_LIST = [S.Pi, S.Exp1, S.GoldenRatio] 16 17 18def search( 19 special_day: str | dt.date, 20 search_start: str | dt.date | None = None, 21 search_end: str | dt.date | None = None, 22 **construct_kwargs, 23) -> set[tuple[Expr, float]]: 24 """ 25 For every day between the date `search_start` and `search_end`, check if the time 26 since the `special_day` can be written as a nerdyversary. If `search_start` is not 27 given, it is assumed to be today. When `search_end` is `None`, it will be 28 `search_start` plus 365 days. 29 30 The keyword arguments `construct_kwargs` are directly passed to the 31 `construct_nerdyversary` function. 32 33 This function returns a list of `candidates`, all of whch contain the analytical 34 `sympy` expression that constitutes the nerdy approximation and the difference as 35 a sanity check. 36 37 Example: 38 >>> search( 39 ... special_day="2016-03-30", 40 ... search_start="2022-07-10", 41 ... search_end="2022-07-20", 42 ... factor_lim=5, 43 ... max_power=3, 44 ... ) 45 [(datetime.date(2022, 7, 12), 2*pi), (datetime.date(2022, 7, 16), 3*pi**3*exp(-2)/2)] 46 """ 47 if isinstance(special_day, str): 48 special_day = dt.date.fromisoformat(special_day) 49 50 if search_start is None: 51 search_start = TODAY 52 elif isinstance(search_start, str): 53 search_start = dt.date.fromisoformat(search_start) 54 55 if search_end is None: 56 search_end = search_start + dt.timedelta(days=365) 57 elif isinstance(search_end, str): 58 search_end = dt.date.fromisoformat(search_end) 59 60 candidates = {} 61 min_duration = (search_start - special_day).days 62 max_duration = (search_end - special_day).days 63 64 for duration_in_days in range(min_duration, max_duration): 65 expressions = construct( 66 duration_in_years=(duration_in_days / DAYS_PER_YEAR), 67 **construct_kwargs 68 ) 69 date = special_day + dt.timedelta(days=duration_in_days) 70 new_candidates = {(date, expr) for expr in expressions} 71 candidates = {*candidates, *new_candidates} 72 73 return sorted(candidates) 74 75 76def construct( 77 duration_in_years: float, 78 symbols: list[NumberSymbol] | None = None, 79 max_power: int = 5, 80 tolerance: float = 0.5, 81 factor_lim: int = 10, 82): 83 """ 84 For a given `duration_in_years`, find nice and nerdy approximations using the list 85 of defined `symbols`. The `max_power` is the largest exponent that is considered 86 for the `symbols`, while `factor_lim` ensures that the factors in the enumerator 87 and denominator don't get too large. An approximation is considred good enough, 88 when it is withing the `tolerance`, which must be given in days. 89 90 Example: 91 >>> duration_in_years = 2 * 3.1416 / 2.7183 92 >>> [res for res in construct(duration_in_years)] 93 [2*pi*exp(-1)] 94 """ 95 if symbols is None: 96 symbols = SYMBOLS_LIST.copy() 97 98 for enum_pow in range(max_power + 1): 99 for denom_pow in range(max_power + 1): 100 # exponents of enum & denom 0 means approximation is fraction 101 if enum_pow == denom_pow == 0: 102 continue 103 104 for enum_sym in symbols: 105 for denom_sym in symbols: 106 # don't consider same symbol in enumerator and denominator 107 if enum_sym == denom_sym: 108 continue 109 110 enum_expr = enum_sym ** enum_pow 111 denom_expr = denom_sym ** denom_pow 112 113 ratio = float((duration_in_years * denom_expr / enum_expr).evalf()) 114 enum_fac, denom_fac = round(ratio, 2).as_integer_ratio() 115 116 if enum_fac > factor_lim or denom_fac > factor_lim: 117 continue 118 119 expression = (enum_fac * enum_expr) / (denom_fac * denom_expr) 120 approx = float(expression.evalf()) 121 difference = np.abs(duration_in_years - approx) 122 123 if difference * DAYS_PER_YEAR < tolerance: 124 yield expression 125 126 127def get_fields(date: dt.date, expression: Expr) -> dict[str, str]: 128 """ 129 Compile a `date`, and the corresponding nerdyversary `expression` into a dictionary 130 that contains the fields `"Date"`, `"Days"`, `"Years"`, and `"Expression"` as a 131 LaTeX formula. 132 133 Example: 134 >>> candidates = search( 135 ... special_day="2016-03-30", 136 ... search_start="2022-07-10", 137 ... search_end="2022-07-20", 138 ... factor_lim=5, 139 ... max_power=3, 140 ... ) 141 >>> get_fields(*candidates[0]) 142 {'Date': '12. Jul 2022', 'Days': '2295', 'Years': '6.28', 'Expression': '$2 \\\\pi$'} 143 144 A list of dictionaries like these for each candidate can then be used to display 145 a pretty table using the [tabulate](https://github.com/astanin/python-tabulate) 146 package. 147 """ 148 duration_in_years = float(expression.evalf()) 149 duration_in_days = round(duration_in_years * DAYS_PER_YEAR) 150 return { 151 "Date" : f"{date:%-d. %b %Y}", 152 "Days" : f"{duration_in_days:d}", 153 "Years" : f"{duration_in_years:.2f}", 154 "Expression": f"${latex(expression)}$", 155 } 156 157 158def main(): 159 """Find beautiful nerdyversaries.""" 160 parser = argparse.ArgumentParser( 161 prog="nerdyversary", 162 description=main.__doc__, 163 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 164 ) 165 parser.add_argument( 166 "-v", "--version", action="version", version=f"nerdyversary {version}", 167 help="Show the installed version and exit." 168 ) 169 parser.add_argument( 170 "-d", "--special-day", type=dt.date.fromisoformat, default=dt.date.today(), 171 help="Date of the special day in ISO format.", 172 ) 173 parser.add_argument( 174 "-s", "--start", type=dt.date.fromisoformat, default=dt.date.today(), 175 help="Date when to start with search in ISO format." 176 ) 177 parser.add_argument( 178 "-e", "--end", type=dt.date.fromisoformat, 179 default=dt.date.today() + dt.timedelta(days=DAYS_PER_YEAR), 180 help="Date when to end the search in ISO format." 181 ) 182 parser.add_argument( 183 "--max-power", type=int, default=5, 184 help="Largest exponent to consider for building the nerdyversaries." 185 ) 186 parser.add_argument( 187 "--factor-lim", type=int, default=10, 188 help="Largest multiple of a symbol that is accepted." 189 ) 190 parser.add_argument( 191 "--format", type=str, default="simple", 192 help="The output format that will be used by the `tabulate` package." 193 ) 194 args = parser.parse_args() 195 196 candidates = search( 197 special_day=args.special_day, 198 search_start=args.start, 199 search_end=args.end, 200 max_power=args.max_power, 201 factor_lim=args.factor_lim, 202 ) 203 table = [get_fields(*candidate) for candidate in candidates] 204 205 print(tabulate(table, headers="keys", tablefmt=args.format))
19def search( 20 special_day: str | dt.date, 21 search_start: str | dt.date | None = None, 22 search_end: str | dt.date | None = None, 23 **construct_kwargs, 24) -> set[tuple[Expr, float]]: 25 """ 26 For every day between the date `search_start` and `search_end`, check if the time 27 since the `special_day` can be written as a nerdyversary. If `search_start` is not 28 given, it is assumed to be today. When `search_end` is `None`, it will be 29 `search_start` plus 365 days. 30 31 The keyword arguments `construct_kwargs` are directly passed to the 32 `construct_nerdyversary` function. 33 34 This function returns a list of `candidates`, all of whch contain the analytical 35 `sympy` expression that constitutes the nerdy approximation and the difference as 36 a sanity check. 37 38 Example: 39 >>> search( 40 ... special_day="2016-03-30", 41 ... search_start="2022-07-10", 42 ... search_end="2022-07-20", 43 ... factor_lim=5, 44 ... max_power=3, 45 ... ) 46 [(datetime.date(2022, 7, 12), 2*pi), (datetime.date(2022, 7, 16), 3*pi**3*exp(-2)/2)] 47 """ 48 if isinstance(special_day, str): 49 special_day = dt.date.fromisoformat(special_day) 50 51 if search_start is None: 52 search_start = TODAY 53 elif isinstance(search_start, str): 54 search_start = dt.date.fromisoformat(search_start) 55 56 if search_end is None: 57 search_end = search_start + dt.timedelta(days=365) 58 elif isinstance(search_end, str): 59 search_end = dt.date.fromisoformat(search_end) 60 61 candidates = {} 62 min_duration = (search_start - special_day).days 63 max_duration = (search_end - special_day).days 64 65 for duration_in_days in range(min_duration, max_duration): 66 expressions = construct( 67 duration_in_years=(duration_in_days / DAYS_PER_YEAR), 68 **construct_kwargs 69 ) 70 date = special_day + dt.timedelta(days=duration_in_days) 71 new_candidates = {(date, expr) for expr in expressions} 72 candidates = {*candidates, *new_candidates} 73 74 return sorted(candidates)
For every day between the date search_start and search_end, check if the time
since the special_day can be written as a nerdyversary. If search_start is not
given, it is assumed to be today. When search_end is None, it will be
search_start plus 365 days.
The keyword arguments construct_kwargs are directly passed to the
construct_nerdyversary function.
This function returns a list of candidates, all of whch contain the analytical
sympy expression that constitutes the nerdy approximation and the difference as
a sanity check.
Example:
>>> search(
... special_day="2016-03-30",
... search_start="2022-07-10",
... search_end="2022-07-20",
... factor_lim=5,
... max_power=3,
... )
[(datetime.date(2022, 7, 12), 2*pi), (datetime.date(2022, 7, 16), 3*pi**3*exp(-2)/2)]
77def construct( 78 duration_in_years: float, 79 symbols: list[NumberSymbol] | None = None, 80 max_power: int = 5, 81 tolerance: float = 0.5, 82 factor_lim: int = 10, 83): 84 """ 85 For a given `duration_in_years`, find nice and nerdy approximations using the list 86 of defined `symbols`. The `max_power` is the largest exponent that is considered 87 for the `symbols`, while `factor_lim` ensures that the factors in the enumerator 88 and denominator don't get too large. An approximation is considred good enough, 89 when it is withing the `tolerance`, which must be given in days. 90 91 Example: 92 >>> duration_in_years = 2 * 3.1416 / 2.7183 93 >>> [res for res in construct(duration_in_years)] 94 [2*pi*exp(-1)] 95 """ 96 if symbols is None: 97 symbols = SYMBOLS_LIST.copy() 98 99 for enum_pow in range(max_power + 1): 100 for denom_pow in range(max_power + 1): 101 # exponents of enum & denom 0 means approximation is fraction 102 if enum_pow == denom_pow == 0: 103 continue 104 105 for enum_sym in symbols: 106 for denom_sym in symbols: 107 # don't consider same symbol in enumerator and denominator 108 if enum_sym == denom_sym: 109 continue 110 111 enum_expr = enum_sym ** enum_pow 112 denom_expr = denom_sym ** denom_pow 113 114 ratio = float((duration_in_years * denom_expr / enum_expr).evalf()) 115 enum_fac, denom_fac = round(ratio, 2).as_integer_ratio() 116 117 if enum_fac > factor_lim or denom_fac > factor_lim: 118 continue 119 120 expression = (enum_fac * enum_expr) / (denom_fac * denom_expr) 121 approx = float(expression.evalf()) 122 difference = np.abs(duration_in_years - approx) 123 124 if difference * DAYS_PER_YEAR < tolerance: 125 yield expression
For a given duration_in_years, find nice and nerdy approximations using the list
of defined symbols. The max_power is the largest exponent that is considered
for the symbols, while factor_lim ensures that the factors in the enumerator
and denominator don't get too large. An approximation is considred good enough,
when it is withing the tolerance, which must be given in days.
Example:
>>> duration_in_years = 2 * 3.1416 / 2.7183
>>> [res for res in construct(duration_in_years)]
[2*pi*exp(-1)]
128def get_fields(date: dt.date, expression: Expr) -> dict[str, str]: 129 """ 130 Compile a `date`, and the corresponding nerdyversary `expression` into a dictionary 131 that contains the fields `"Date"`, `"Days"`, `"Years"`, and `"Expression"` as a 132 LaTeX formula. 133 134 Example: 135 >>> candidates = search( 136 ... special_day="2016-03-30", 137 ... search_start="2022-07-10", 138 ... search_end="2022-07-20", 139 ... factor_lim=5, 140 ... max_power=3, 141 ... ) 142 >>> get_fields(*candidates[0]) 143 {'Date': '12. Jul 2022', 'Days': '2295', 'Years': '6.28', 'Expression': '$2 \\\\pi$'} 144 145 A list of dictionaries like these for each candidate can then be used to display 146 a pretty table using the [tabulate](https://github.com/astanin/python-tabulate) 147 package. 148 """ 149 duration_in_years = float(expression.evalf()) 150 duration_in_days = round(duration_in_years * DAYS_PER_YEAR) 151 return { 152 "Date" : f"{date:%-d. %b %Y}", 153 "Days" : f"{duration_in_days:d}", 154 "Years" : f"{duration_in_years:.2f}", 155 "Expression": f"${latex(expression)}$", 156 }
Compile a date, and the corresponding nerdyversary expression into a dictionary
that contains the fields "Date", "Days", "Years", and "Expression" as a
LaTeX formula.
Example:
>>> candidates = search(
... special_day="2016-03-30",
... search_start="2022-07-10",
... search_end="2022-07-20",
... factor_lim=5,
... max_power=3,
... )
>>> get_fields(*candidates[0])
{'Date': '12. Jul 2022', 'Days': '2295', 'Years': '6.28', 'Expression': '$2 \\pi$'}
A list of dictionaries like these for each candidate can then be used to display a pretty table using the tabulate package.
159def main(): 160 """Find beautiful nerdyversaries.""" 161 parser = argparse.ArgumentParser( 162 prog="nerdyversary", 163 description=main.__doc__, 164 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 165 ) 166 parser.add_argument( 167 "-v", "--version", action="version", version=f"nerdyversary {version}", 168 help="Show the installed version and exit." 169 ) 170 parser.add_argument( 171 "-d", "--special-day", type=dt.date.fromisoformat, default=dt.date.today(), 172 help="Date of the special day in ISO format.", 173 ) 174 parser.add_argument( 175 "-s", "--start", type=dt.date.fromisoformat, default=dt.date.today(), 176 help="Date when to start with search in ISO format." 177 ) 178 parser.add_argument( 179 "-e", "--end", type=dt.date.fromisoformat, 180 default=dt.date.today() + dt.timedelta(days=DAYS_PER_YEAR), 181 help="Date when to end the search in ISO format." 182 ) 183 parser.add_argument( 184 "--max-power", type=int, default=5, 185 help="Largest exponent to consider for building the nerdyversaries." 186 ) 187 parser.add_argument( 188 "--factor-lim", type=int, default=10, 189 help="Largest multiple of a symbol that is accepted." 190 ) 191 parser.add_argument( 192 "--format", type=str, default="simple", 193 help="The output format that will be used by the `tabulate` package." 194 ) 195 args = parser.parse_args() 196 197 candidates = search( 198 special_day=args.special_day, 199 search_start=args.start, 200 search_end=args.end, 201 max_power=args.max_power, 202 factor_lim=args.factor_lim, 203 ) 204 table = [get_fields(*candidate) for candidate in candidates] 205 206 print(tabulate(table, headers="keys", tablefmt=args.format))
Find beautiful nerdyversaries.