Skip to content

Commit 53ccc4d

Browse files
committed
Initial commit
0 parents  commit 53ccc4d

18 files changed

+1223
-0
lines changed

.gitignore

Lines changed: 518 additions & 0 deletions
Large diffs are not rendered by default.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Tifa
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Makefile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
lang = zh-hans en
2+
latexmk = latexmk -cd -xelatex -synctex=1 -interaction=nonstopmode -file-line-error -halt-on-error -outdir=out -time
3+
filter = python filter.py -l DEBUG
4+
texgen = python tex-gen.py -l DEBUG
5+
6+
all: gentex pdf
7+
.PHONY: gentex pdf $(lang) clean
8+
9+
pdf: $(lang)
10+
11+
gentex:
12+
$(filter) clean
13+
$(texgen) gen
14+
15+
clean:
16+
-rm -rf data
17+
-rm tex/oeis.tex
18+
-mkdir data
19+
-mkdir data/detail
20+
-touch data/.gitkeep
21+
-touch data/detail/.gitkeep
22+
cd tex && (latexmk -C -outdir=out *.tex; cd ..)
23+
24+
$(lang): % : tex/oeis-pdf-%.tex
25+
$(latexmk) $<

README.md

Lines changed: 54 additions & 0 deletions
Large diffs are not rendered by default.

data/.gitkeep

Whitespace-only changes.

data/detail/.gitkeep

Whitespace-only changes.

doc/README-zh-hans.md

Lines changed: 54 additions & 0 deletions
Large diffs are not rendered by default.

filter.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#!/usr/bin/python3
2+
# -*- coding: utf-8 -*-
3+
4+
import json
5+
import os
6+
import re
7+
8+
import click
9+
import coloredlogs
10+
from libs.decorator import withlog
11+
from libs.predicate import *
12+
13+
14+
@click.group()
15+
@click.option('-l', '--level', type=click.Choice(['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']), help='log level',
16+
default='INFO')
17+
def cli(level: str):
18+
coloredlogs.install(milliseconds=True,
19+
level=level,
20+
fmt='%(asctime)s:%(msecs)03d %(levelname)s %(programname)s::%(name)s [%(process)d] %(message)s',
21+
field_styles={'asctime': {'color': 'green'},
22+
'msecs': {'color': 'green'},
23+
'hostname': {'color': 'red'},
24+
'levelname': {'bold': True, 'color': 'magenta'},
25+
'name': {'faint': True, 'color': 'blue'},
26+
'programname': {'bold': True, 'color': 'cyan'},
27+
'process': {'faint': True, 'color': 'green'},
28+
'username': {'color': 'yellow'}},
29+
level_styles={'critical': {'bold': True, 'color': 'red'},
30+
'debug': {'color': 'cyan'},
31+
'error': {'color': 'red'},
32+
'info': {'bright': True, 'color': 'white'},
33+
'notice': {'color': 'magenta'},
34+
'spam': {'color': 'green', 'faint': True},
35+
'success': {'bold': True, 'color': 'green'},
36+
'verbose': {'color': 'blue'},
37+
'warning': {'bright': True, 'color': 'yellow'}})
38+
39+
40+
RE_OEIS_ID = re.compile(r'A\d+')
41+
42+
43+
@cli.command('clean')
44+
@click.argument('folder', type=click.Path(exists=True), default='data/detail')
45+
def _filter_results(folder):
46+
"""Filter results
47+
48+
Remove all sequences with no valid formula"""
49+
50+
def check(_single_result: dict) -> bool:
51+
return 'formula' in _single_result.keys() and 'data' in _single_result.keys()
52+
53+
@withlog
54+
def filter_results(_result_folder: str, **kwargs):
55+
logger = kwargs.get('logger')
56+
57+
__remove_cnt, __clean_cnt, __total_cnt = 0, 0, sum(
58+
len(filenames) for _, __, filenames in os.walk(_result_folder))
59+
__flag: bool = True
60+
61+
while __flag:
62+
__flag = False
63+
__id_set: set = set()
64+
for dir_path, _, filenames in os.walk(_result_folder):
65+
__id_set = __id_set.union(
66+
x.removesuffix('.json') for x in filter(lambda x: x.endswith('.json'), filenames))
67+
68+
for dir_path, _, filenames in os.walk(_result_folder):
69+
for name in filter(lambda x: x.endswith('.json'), filenames):
70+
filename = os.path.join(dir_path, name)
71+
logger.debug(f'reading {filename}')
72+
now_json: dict = json.load(open(filename, 'r'))
73+
if not check(now_json):
74+
logger.debug(
75+
f'remove {filename} because of failing check')
76+
os.remove(filename)
77+
__remove_cnt += 1
78+
__flag = True
79+
else:
80+
new_formulas: list[str] = list(
81+
filter(lambda x: all(i in __id_set for i in re.findall(RE_OEIS_ID, x)),
82+
now_json['formula']))
83+
if new_formulas:
84+
if now_json['formula'] != new_formulas:
85+
__clean_cnt += 1
86+
now_json['formula'] = new_formulas
87+
json.dump(now_json, open(filename, 'w'))
88+
else:
89+
logger.debug(
90+
f'remove {filename} because of no valid formula')
91+
os.remove(filename)
92+
__remove_cnt += 1
93+
__flag = True
94+
95+
logger.info(f'{__remove_cnt} / {__total_cnt} result(s) removed')
96+
logger.info(f'{__clean_cnt} / {__total_cnt} result(s) cleaned')
97+
98+
filter_results(click.format_filename(folder))
99+
100+
101+
@cli.command('rmbad')
102+
@click.argument('folder', type=click.Path(exists=True), default='data/detail')
103+
def _remove_bad_results(folder):
104+
"""Remove bad results"""
105+
106+
@withlog
107+
def remove_bad_results(_result_folder: str, **kwargs):
108+
logger = kwargs.get('logger')
109+
110+
__remove_cnt, __total_cnt = 0, 0
111+
for dir_path, _, filenames in os.walk(_result_folder):
112+
for name in filter(lambda x: x.endswith('.json'), filenames):
113+
filename = os.path.join(dir_path, name)
114+
__total_cnt += 1
115+
try:
116+
if not os.path.getsize(filename):
117+
raise Exception(f'Empty file: {filename}')
118+
if json.load(open(filename, 'r'))['number'] != int(name.removeprefix('A').removesuffix('.json')):
119+
raise Exception('Invalid result')
120+
except Exception as e:
121+
os.remove(filename)
122+
__remove_cnt += 1
123+
logger.info(f'{__remove_cnt} / {__total_cnt} bad result(s) removed')
124+
125+
remove_bad_results(click.format_filename(folder))
126+
127+
128+
@cli.command('reduce')
129+
@click.option('-k', '--key', required=True, multiple=True, help='Key which will be removed')
130+
@click.argument('folder', type=click.Path(exists=True), default='data/detail')
131+
def _reduce_results(key: tuple[str, ...], folder):
132+
"""Remove some keys in results"""
133+
134+
@withlog
135+
def reduce_results(_key_removed: set[str], _result_folder: str, **kwargs):
136+
logger = kwargs.get('logger')
137+
138+
for dir_path, _, filenames in os.walk(_result_folder):
139+
for name in filter(lambda x: x.endswith('.json'), filenames):
140+
filename = os.path.join(dir_path, name)
141+
json.dump(dict([(k, v) for k, v in json.load(open(filename, 'r')).items() if k not in _key_removed]),
142+
open(filename, 'w'))
143+
144+
reduce_results(set(key), click.format_filename(folder))
145+
146+
147+
if __name__ == '__main__':
148+
cli()

libs/CommandGenerator.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import re
2+
3+
RE_BRACKET_NEEDS_LSTRIP = re.compile(r'\s+([(\[{])')
4+
RE_BRACKET_NEEDS_RSTRIP = re.compile(r'([)\]}])\s+')
5+
RE_RELATION = re.compile(r'\s*([<>:!=]=|=|<|>)\s*')
6+
RE_FORMULA_AN = re.compile(r'^a\s*\(\s*n\s*\)\s*=\s*', flags=re.IGNORECASE)
7+
RE_FORMULA_GF = re.compile(r'^g\.f\.\s*:?\s*', flags=re.IGNORECASE)
8+
RE_FORMULA_EGF = re.compile(r'^e\.g\.f\.\s*:?\s*', flags=re.IGNORECASE)
9+
RE_FORMULA_DGF = re.compile(
10+
r'^d(?:\.|irichlet)\s*g\.f\.\s*:?\s*', flags=re.IGNORECASE)
11+
RE_AUTHOR_DATE_IN_THE_END = re.compile(
12+
r'-\s*_(?P<author>(?:[A-Za-z.\-]+?\s*)+?)_,?\s*(?P<date>(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d+\s+\d+)')
13+
14+
15+
class CommandGenerator:
16+
def __init__(self, _id: str, seq_detail: dict) -> None:
17+
if int(_id.removeprefix('A')) != seq_detail['number']:
18+
raise RuntimeError(
19+
f"ID match error. ID input = {_id}, ID in seq_detail = {seq_detail['number']}")
20+
21+
self.id: str = _id
22+
self.offset: str = seq_detail.get('offset', '')
23+
self.name: str = seq_detail.get('name', '')
24+
25+
__data: list[int] = [int(i) for i in seq_detail['data'].split(',')]
26+
self.cnt0: int = __data.index(
27+
next(filter(lambda x: x > 0, __data), __data[-1]))
28+
if __data[self.cnt0] == 0:
29+
self.cnt0 = len(__data)
30+
__data = __data[self.cnt0:]
31+
if __data:
32+
self.cnt1: int = __data.index(
33+
next(filter(lambda x: x > 1, __data), __data[-1]))
34+
if __data[self.cnt1] == 1:
35+
self.cnt1 = len(__data)
36+
self.data: list[int] = __data[self.cnt1:]
37+
else:
38+
self.cnt1: int = 0
39+
self.data: list[int] = []
40+
41+
self.formula: list[str] = []
42+
self.formula_an: list[str] = []
43+
self.formula_gf: list[str] = []
44+
self.formula_egf: list[str] = []
45+
self.formula_dgf: list[str] = []
46+
47+
for now_formula in [i.strip() for i in seq_detail['formula']]:
48+
now_formula = re.sub(RE_AUTHOR_DATE_IN_THE_END, '', now_formula)
49+
if re.search(RE_FORMULA_AN, now_formula):
50+
self.formula_an.append(re.sub(RE_FORMULA_AN, '', now_formula))
51+
continue
52+
if re.search(RE_FORMULA_GF, now_formula):
53+
self.formula_gf.append(re.sub(RE_FORMULA_GF, '', now_formula))
54+
continue
55+
if re.search(RE_FORMULA_EGF, now_formula):
56+
self.formula_egf.append(
57+
re.sub(RE_FORMULA_EGF, '', now_formula))
58+
continue
59+
if re.search(RE_FORMULA_DGF, now_formula):
60+
self.formula_dgf.append(
61+
re.sub(RE_FORMULA_DGF, '', now_formula))
62+
continue
63+
self.formula.append(now_formula)
64+
65+
def __lt__(self, rhs) -> bool:
66+
if self.data != rhs.data:
67+
return self.data < rhs.data
68+
if self.cnt1 != rhs.cnt1:
69+
return self.cnt1 < rhs.cnt1
70+
if self.cnt0 != rhs.cnt0:
71+
return self.cnt0 < rhs.cnt0
72+
return self.id < rhs.id
73+
74+
@staticmethod
75+
def __plain_text(formula: str) -> str:
76+
def __f(__s: str) -> str:
77+
return ''.join(rf'\{ch}' if ch in r'#$%{}_&' else r'\;' if ch == ' ' else ch for ch in __s.strip())
78+
79+
formula = re.sub(RE_BRACKET_NEEDS_LSTRIP,
80+
lambda x: x.group(1), formula)
81+
formula = re.sub(RE_BRACKET_NEEDS_RSTRIP,
82+
lambda x: x.group(1), formula)
83+
formula = re.sub(RE_RELATION, lambda x: f' {x.group(1)} ', formula)
84+
85+
return ' '.join(filter(lambda x: x, r' \^{} '.join(r' \~{} '.join(
86+
r' \(\backslash\) '.join(rf' \seqsplit{{{__f(part3)}}} ' for part3 in part2.split('\\')) for part2 in
87+
part1.split('~')) for part1 in formula.split('^')).split()))
88+
89+
def __seq_data(self) -> str:
90+
result: str = rf"\{{{str(self.cnt0) if self.cnt0 else ''},{str(self.cnt1) if self.cnt1 else ''}\}}" if self.cnt0 or self.cnt1 else ''
91+
__data: list[int] = self.data
92+
__cnt: int = 0
93+
while __data:
94+
if len(__data) < 2 or __data[0] != __data[1]:
95+
result += f',{__data[0]}'
96+
__data = __data[1:]
97+
__cnt += 1
98+
if __cnt >= 10:
99+
break
100+
101+
if __data:
102+
continue
103+
else:
104+
break
105+
106+
__idx: int = __data.index(
107+
next(filter(lambda x: x != __data[0], __data), __data[-1]))
108+
if not __idx:
109+
__idx = len(__data)
110+
111+
result += f',{__data[0]}:{__idx}' if __idx > 1 else f',{__data[0]}'
112+
__data = __data[__idx:]
113+
__cnt += 1
114+
if __cnt >= 10:
115+
break
116+
117+
return rf"\seqsplit{{{result.strip(',')}}}"
118+
119+
def str_tex(self) -> str:
120+
return ' '.join([rf'\textbf{{{self.id}}}\index{{{self.id}}}' + (rf'\(\langle {self.offset}\rangle\)' if self.offset else ''),
121+
rf'{{\ttfamily {self.__seq_data()}}}',
122+
rf'\textit{{{self.__plain_text(self.name)}}}'] +
123+
[rf'\(\ddagger\) {self.__plain_text(i)}' for i in self.formula_an] +
124+
[rf'\(\flat\) {self.__plain_text(i)}' for i in self.formula_gf] +
125+
[rf'\(\natural\) {self.__plain_text(i)}' for i in self.formula_egf] +
126+
[rf'\(\diamond\) {self.__plain_text(i)}' for i in self.formula_dgf] +
127+
[rf'\(\dagger\) {self.__plain_text(i)}' for i in self.formula] +
128+
[rf'\href{{\oeis {self.id}/}}{{\P}}'])

libs/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)