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/nerdyversary
Create a Virtual Environment (optional, but recommended)
$ python3 -m venv .venv
You should do this with an installation of Python 3.10 or later. And don't forget to activate the environment with
$ source .venv/bin/activate
Use
pip
to 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.