nerdyversary

Two Pi Pies

Nerdyversary

MIT license GitHub repo build badge docs badge tests badge

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:

  1. Clone the repository

    $ git clone https://github.com/rmnldwg/nerdyversary
    
  2. 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
    
  3. 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))
def construct( duration_in_years: float, symbols: list[sympy.core.numbers.NumberSymbol] | None = None, max_power: int = 5, tolerance: float = 0.5, factor_lim: int = 10):
 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)]
def get_fields(date: datetime.date, expression: sympy.core.expr.Expr) -> dict[str, str]:
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.

def main():
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.