├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .pylintrc ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── borax ├── __init__.py ├── calendars │ ├── __init__.py │ ├── birthday.py │ ├── data_parser.py │ ├── dataset │ │ ├── FestivalData.csv │ │ ├── __init__.py │ │ └── festivals_ext1.csv │ ├── datepicker.py │ ├── festivals2.py │ ├── lunardate.py │ ├── ui.py │ └── utils.py ├── capp │ ├── __init__.py │ ├── __main__.py │ ├── borax_calendar_app.py │ └── festival_creator.py ├── choices.py ├── constants.py ├── counters │ ├── __init__.py │ ├── daily.py │ └── serial_pool.py ├── datasets │ ├── __init__.py │ ├── fetch.py │ └── join_.py ├── devtools.py ├── htmls.py ├── numbers.py ├── patterns │ ├── __init__.py │ ├── lazy.py │ ├── proxy.py │ └── singleton.py ├── serialize │ ├── __init__.py │ └── cjson.py ├── strings.py ├── structures │ ├── __init__.py │ ├── dictionary.py │ ├── percentage.py │ └── tree.py ├── system.py ├── ui │ ├── __init__.py │ ├── aiotk.py │ └── widgets.py └── utils.py ├── codecov.yml ├── docs ├── .nojekyll ├── README.md ├── capp_changelog.md ├── changelog.md ├── develop_note.md ├── guides │ ├── birthday.md │ ├── bjson.md │ ├── borax_calendar_app.md │ ├── calendars-utils.md │ ├── choices.md │ ├── cjson.md │ ├── festival.md │ ├── festivals2-library.md │ ├── festivals2-serialize.md │ ├── festivals2-ui.md │ ├── festivals2-usage.md │ ├── festivals2.md │ ├── fetch.md │ ├── join.md │ ├── lunardate.md │ ├── numbers.md │ ├── percentage.md │ ├── serial_generator.md │ ├── serial_pool.md │ ├── singleton.md │ ├── strings.md │ ├── tree.md │ └── ui.md ├── images │ ├── app_borax_calendar.png │ ├── app_festival_creator.png │ ├── calendar_frame.png │ ├── donation-wechat.png │ ├── festival_table.png │ └── image1.png ├── posts │ └── lunardate-development.md └── release-note │ ├── v350.md │ ├── v356.md │ ├── v400.md │ └── v410.md ├── mkdocs.yaml ├── pyproject.toml ├── requirements_dev.txt ├── requirements_doc.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_birthday.py ├── test_calendars.py ├── test_chinese_number.py ├── test_choices.py ├── test_daily_counter.py ├── test_date_pickle.py ├── test_dictionary.py ├── test_festival2_list.py ├── test_festival_library.py ├── test_festivals2_serialize.py ├── test_festivals_datasets.csv ├── test_fetch.py ├── test_finance.py ├── test_htmls.py ├── test_json.py ├── test_lazy.py ├── test_lunar_benchmark.py ├── test_lunar_parser.py ├── test_lunar_sqlite3_feature.py ├── test_lunardate.py ├── test_new_join.py ├── test_percentage.py ├── test_proxy.py ├── test_runtime_measurer.py ├── test_serial_pool.py ├── test_singleton.py ├── test_string_convert.py ├── test_tree_fetcher.py └── test_utils.py └── tools └── validate_lunar.py /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | - develop 11 | - dev/v** 12 | - release/v** 13 | pull_request: 14 | branches: 15 | - master 16 | - develop 17 | 18 | jobs: 19 | build: 20 | 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v2 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | - name: Install dependencies 33 | run: | 34 | python -m pip install --upgrade pip 35 | if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi 36 | - name: Lint with flake8 37 | run: | 38 | flake8 borax tests 39 | - name: Test with nose2 40 | run: | 41 | nose2 --with-coverage --coverage borax --coverage-report xml 42 | - name: Upload coverage to Codecov 43 | uses: codecov/codecov-action@v1 44 | with: 45 | token: ${{secrets.CODECOV_TOKEN}} 46 | file: ./coverage.xml 47 | env_vars: OS,PYTHON 48 | name: codecov-umbrella 49 | fail_ci_if_error: true 50 | verbose: true 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | .idea 104 | 105 | node_modules 106 | *.code-workspace 107 | 108 | demo.py 109 | 110 | # Visual Studio Code 111 | *.vscode -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | 14 | mkdocs: 15 | configuration: mkdocs.yaml 16 | 17 | # Optionally declare the Python requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: requirements_doc.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2025 kinegratii 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include borax/calendars/dataset/FestivalData.csv 2 | include borax/calendars/dataset/festivals_ext1.csv -------------------------------------------------------------------------------- /borax/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '4.1.3' 2 | __author__ = 'kinegratii' 3 | -------------------------------------------------------------------------------- /borax/calendars/__init__.py: -------------------------------------------------------------------------------- 1 | from .lunardate import LunarDate, InvalidLunarDateError, LCalendars 2 | from .utils import SCalendars 3 | 4 | __all__ = ['LunarDate', 'InvalidLunarDateError', 'LCalendars', 'SCalendars'] 5 | -------------------------------------------------------------------------------- /borax/calendars/birthday.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import date, timedelta 3 | from typing import Union 4 | 5 | from .festivals2 import WrappedDate, LunarFestival, SolarFestival 6 | from .lunardate import LunarDate, LCalendars 7 | 8 | 9 | def nominal_age(birthday, today=None): 10 | birthday = LCalendars.cast_date(birthday, LunarDate) 11 | if today: 12 | today = LCalendars.cast_date(today, LunarDate) 13 | else: 14 | today = LunarDate.today() 15 | return today.year - birthday.year + 1 16 | 17 | 18 | def actual_age_solar(birthday, today=None): 19 | """See more at https://stackoverflow.com/questions/2217488/age-from-birthdate-in-python/9754466#9754466 20 | :param birthday: 21 | :param today: 22 | :return: 23 | """ 24 | birthday = LCalendars.cast_date(birthday, date) 25 | if today: 26 | today = LCalendars.cast_date(today, date) 27 | else: 28 | today = date.today() 29 | return today.year - birthday.year - ((today.month, today.day) < (birthday.month, birthday.day)) 30 | 31 | 32 | def actual_age_lunar(birthday, today=None): 33 | birthday = LCalendars.cast_date(birthday, LunarDate) 34 | if today: 35 | today = LCalendars.cast_date(today, LunarDate) 36 | else: 37 | today = LunarDate.today() 38 | day_flag = (today.month, today.leap, today.day) < (birthday.month, birthday.leap, birthday.day) 39 | return today.year - birthday.year - day_flag 40 | 41 | 42 | @dataclass 43 | class BirthdayResult: 44 | nominal_age: int = 0 # 虚岁 45 | actual_age: int = 0 # 周岁 46 | animal: str = '' 47 | birthday_str: str = '' 48 | next_solar_birthday: WrappedDate = None 49 | next_lunar_birthday: WrappedDate = None 50 | living_day_count: int = 0 51 | 52 | 53 | class BirthdayCalculator: 54 | def __init__(self, birthday: Union[date, LunarDate]): 55 | self._birthday: WrappedDate = WrappedDate(birthday) 56 | self._sbf = SolarFestival(freq='y', month=self._birthday.solar.month, day=self._birthday.solar.day) 57 | self._lbf = LunarFestival(freq='y', month=self._birthday.lunar.month, day=self._birthday.lunar.day, 58 | leap=self._birthday.lunar.leap) 59 | 60 | @property 61 | def birthday(self) -> WrappedDate: 62 | return self._birthday 63 | 64 | @property 65 | def solar_birthday_festival(self) -> SolarFestival: 66 | return self._sbf 67 | 68 | @property 69 | def lunar_birthday_festival(self) -> LunarFestival: 70 | return self._lbf 71 | 72 | def calculate(self, this_day=None) -> BirthdayResult: 73 | """Calculate one's age and birthday info based on a given date.""" 74 | if this_day is None: 75 | this_date = WrappedDate(date.today()) 76 | else: 77 | this_date = WrappedDate(this_day) 78 | result = BirthdayResult(animal=self._birthday.lunar.animal, birthday_str=self._birthday.full_str()) 79 | result.nominal_age = nominal_age(self._birthday, this_date.lunar) 80 | result.actual_age = actual_age_solar(self._birthday, this_date.solar) 81 | result.next_lunar_birthday = self._lbf.list_days(start_date=this_date, count=1)[0] 82 | result.next_solar_birthday = self._sbf.list_days(start_date=this_date, count=1)[0] 83 | result.living_day_count = (this_date - self._birthday).days 84 | return result 85 | 86 | def list_days_in_same_day(self, start_date=None, end_date=None) -> list[WrappedDate]: 87 | """Return the days in a same days by solar and lunar birthday""" 88 | if start_date is None: 89 | start_date = self.birthday + timedelta(days=1) 90 | wrapped_date_list = [] 91 | for wd in self._sbf.list_days(start_date, end_date): 92 | if wd.lunar.month == self.birthday.lunar.month and wd.lunar.day == self.birthday.lunar.day: 93 | wrapped_date_list.append(wd) 94 | return wrapped_date_list 95 | -------------------------------------------------------------------------------- /borax/calendars/data_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | from borax.calendars.lunardate import LunarDate 3 | 4 | __all__ = ['strptime'] 5 | 6 | 7 | class LunarDateParser(dict): 8 | 9 | def __init__(self): 10 | super().__init__({ 11 | 'y': r'(?P\d{4})', 12 | 'l': r'(?P[01])', 13 | 'm': r'(?P1[0-2]|[1-9])', 14 | 'A': r'(?P1[0-2]|0[1-9]|[1-9])', 15 | 'd': r'(?P3[0-1]|2[0-9]|1[0-9]|[1-9])', 16 | 'B': r'(?P3[0-1]|2[0-9]|1[0-9]|[1-9])|0[1-9]', 17 | 'Y': r'(?P[〇一二三四五六七八九十]{4})', 18 | 'L': r'(?P闰?)', 19 | 'M': r'(?P[正一二三四五六七八九十冬腊])', 20 | 'D': r'(?P卅[一二]|廿[一二三四五六七八九]|十[一二三四五六七八九]|初[一二三四五六七八九十]|[二三]十)' 21 | }) 22 | 23 | def pattern(self, fmt): 24 | fmt = fmt.replace('%C', '%Y年%L%M月%D') 25 | return self._pattern(fmt) 26 | 27 | def _pattern(self, fmt): 28 | processed_format = '' 29 | # The sub() call escapes all characters that might be misconstrued 30 | # as regex syntax. Cannot use re.escape since we have to deal with 31 | # format directives (%m, etc.). 32 | regex_chars = re.compile(r"([\\.^$*+?\(\){}\[\]|])") 33 | fmt = regex_chars.sub(r"\\\1", fmt) 34 | whitespace_replacement = re.compile(r'\s+') 35 | fmt = whitespace_replacement.sub(r'\\s+', fmt) 36 | while '%' in fmt: 37 | directive_index = fmt.index('%') + 1 38 | processed_format = "%s%s%s" % (processed_format, 39 | fmt[:directive_index - 1], 40 | self[fmt[directive_index]]) 41 | fmt = fmt[directive_index + 1:] 42 | return "%s%s" % (processed_format, fmt) 43 | 44 | def compile(self, fmt): 45 | return re.compile(self.pattern(fmt), re.IGNORECASE) 46 | 47 | def parse(self, data_string, date_format): 48 | 49 | format_regex = _cache_dic.get(date_format) 50 | if not format_regex: 51 | format_regex = _parser.compile(date_format) 52 | _cache_dic[date_format] = format_regex 53 | 54 | found = format_regex.match(data_string) 55 | if not found: 56 | raise ValueError("time data %r does not match format %r" % 57 | (data_string, date_format)) 58 | if len(data_string) != found.end(): 59 | raise ValueError("unconverted data remains: %s" % 60 | data_string[found.end():]) 61 | found_dict = found.groupdict() 62 | 63 | year = self.validate_and_return(found_dict, 'yY', 'year') 64 | leap = self.validate_and_return(found_dict, 'lL', 'leap', 0) # Optional field 65 | month = self.validate_and_return(found_dict, 'mMA', 'month') 66 | day = self.validate_and_return(found_dict, 'dDB', 'day') 67 | return LunarDate(year, month, day, leap) 68 | 69 | def validate_and_return(self, found_dict, fields, attr_name, default_value=None): 70 | values = [] 71 | for f in fields: 72 | if f not in found_dict: 73 | continue 74 | raw_value = found_dict[f] 75 | try: 76 | value = getattr(self, f'convert_{f}')(raw_value) 77 | except AttributeError: 78 | if raw_value == '' and default_value is not None: 79 | value = default_value 80 | else: 81 | value = int(raw_value) 82 | values.append(value) 83 | if len(values) == 0: 84 | if default_value is not None: 85 | return default_value 86 | raise ValueError(f'Can not find any value match with "{attr_name}".') 87 | if all(ele == values[0] for ele in values): 88 | return values[0] 89 | else: 90 | raise ValueError(f'Multiple different values found with "{attr_name}".') 91 | 92 | def convert_Y(self, raw_value): 93 | chars = '〇一二三四五六七八九十' 94 | return int(''.join([str(chars.index(c)) for c in raw_value])) 95 | 96 | def convert_L(self, raw_value): 97 | if raw_value == '闰': 98 | return 1 99 | else: 100 | return 0 101 | 102 | def convert_M(self, raw_value): 103 | return 'X正二三四五六七八九十冬腊'.index(raw_value) 104 | 105 | def convert_D(self, raw_value): 106 | charts = '十一二三四五六七八九' 107 | tens = '初十廿卅' 108 | if raw_value[1] == '十': 109 | return 'X初二三'.index(raw_value[0]) * 10 110 | return tens.index(raw_value[0]) * 10 + charts.index(raw_value[1]) 111 | 112 | 113 | _parser = LunarDateParser() 114 | _cache_dic = {} 115 | 116 | 117 | def strptime(data_string: str, date_format: str) -> LunarDate: 118 | """Parse a LunarDate object from a whole string.""" 119 | return _parser.parse(data_string, date_format) 120 | -------------------------------------------------------------------------------- /borax/calendars/dataset/FestivalData.csv: -------------------------------------------------------------------------------- 1 | 001010,元旦 2 | 002140,情人节 3 | 003080,妇女节 4 | 003120,植树节 5 | 004010,愚人节 6 | 005010,劳动节 7 | 005040,青年节 8 | 005120,护士节 9 | 006010,儿童节 10 | 008010,建军节 11 | 009100,教师节 12 | 010010,国庆节 13 | 012240,平安夜 14 | 012250,圣诞节 15 | 101010,春节 16 | 101150,元宵节 17 | 102020,龙头节 18 | 103030,上巳节 19 | 105050,端午节 20 | 107070,七夕 21 | 107150,中元节 22 | 108150,中秋节 23 | 109090,重阳节 24 | 112080,腊八节 25 | 10001A,除夕 26 | 205026,母亲节 27 | 206036,父亲节 28 | 211043,感恩节 29 | 400060,清明 30 | 400230,冬至 31 | 413116,初伏 32 | 414116,中伏 33 | 411146,末伏 -------------------------------------------------------------------------------- /borax/calendars/dataset/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | __all__ = ['get_festival_dataset_path'] 4 | 5 | _FILE_DICT = { 6 | 'basic': 'FestivalData.csv', 7 | 'ext1': 'festivals_ext1.csv', 8 | 'zh-Hans': 'FestivalData.csv' 9 | } 10 | 11 | 12 | def get_festival_dataset_path(identifier: str) -> Path: 13 | """Return the full path for festival dataset csv file.""" 14 | if identifier not in _FILE_DICT: 15 | raise ValueError(f'Festival Dataset {identifier} not found!') 16 | return Path(__file__).parent / _FILE_DICT.get(identifier) 17 | -------------------------------------------------------------------------------- /borax/calendars/datepicker.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module for datepicker. 3 | """ 4 | 5 | import tkinter as tk 6 | from typing import Optional 7 | 8 | from borax.calendars.festivals2 import WrappedDate 9 | from borax.calendars.ui import CalendarFrame 10 | 11 | __all__ = ['DatePickerDialog', 'ask_date'] 12 | 13 | 14 | class DatePickerDialog(tk.Toplevel): 15 | """A dialog for datepicker.""" 16 | 17 | def __init__(self, parent=None, modal=True): 18 | tk.Toplevel.__init__(self, parent=None) 19 | self.title('选择日期') 20 | self._parent = parent 21 | self._modal = modal 22 | self._cf = CalendarFrame(self) 23 | self._cf.bind_date_selected(self._set_date) 24 | self._cf.pack(side='left') 25 | self._selected_date = None 26 | 27 | def _set_date(self, wd: WrappedDate): 28 | self._selected_date = wd 29 | self.destroy() 30 | 31 | def show(self) -> Optional[WrappedDate]: 32 | if self._modal: 33 | self.transient(self._parent) 34 | self.grab_set() 35 | self.wait_window() 36 | return self._selected_date 37 | 38 | 39 | def ask_date() -> Optional[WrappedDate]: 40 | """Open a date picker dialog.""" 41 | return DatePickerDialog().show() 42 | -------------------------------------------------------------------------------- /borax/calendars/utils.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | from collections import OrderedDict 3 | from datetime import date, datetime, timedelta 4 | from typing import Union, Dict 5 | 6 | from borax.calendars.lunardate import LunarDate, TextUtils, TermUtils 7 | 8 | __all__ = ['SCalendars', 'ThreeNineUtils'] 9 | 10 | 11 | class SCalendars: 12 | @staticmethod 13 | def get_last_day_of_this_month(year: int, month: int) -> date: 14 | return date(year, month, calendar.monthrange(year, month)[-1]) 15 | 16 | @staticmethod 17 | def get_fist_day_of_year_week(year: int, week: int) -> date: 18 | fmt = f'{year}-W{week}-1' 19 | return datetime.strptime(fmt, "%Y-W%W-%w").date() 20 | 21 | 22 | class ThreeNineUtils: 23 | """三伏数九天工具函数 24 | """ 25 | 26 | @staticmethod 27 | def get_39days(year: int) -> Dict[str, date]: 28 | """获取公历year年的三伏数九天对应的公历日期。 29 | """ 30 | day13 = TermUtils.day_start_from_term(year, '夏至', 3, '庚') 31 | day23 = day13 + timedelta(days=10) 32 | day33 = TermUtils.day_start_from_term(year, '立秋', 1, '庚') 33 | day19 = TermUtils.day_start_from_term(year, '冬至', 0) 34 | days = OrderedDict({ 35 | '初伏': day13, 36 | '中伏': day23, 37 | '末伏': day33, 38 | '一九': day19 39 | }) 40 | for i, dc in enumerate(TextUtils.DAYS_CN[1:10], start=1): 41 | days[f'{dc}九'] = day19 + timedelta(days=(i - 1) * 9) 42 | return days 43 | 44 | @staticmethod 45 | def get_39label(date_obj: Union[date, LunarDate]) -> str: 46 | """返回三伏数九天对应的标签,如果不是,返回空字符串。 47 | """ 48 | if isinstance(date_obj, LunarDate): 49 | sd = date_obj.to_solar_date() 50 | else: 51 | sd = date_obj 52 | if sd.month in (4, 5, 6, 10, 11): 53 | return '' 54 | year = sd.year - bool(sd.month < 4) 55 | days = ThreeNineUtils.get_39days(year) 56 | for vs in list(days.items()): 57 | label, sd = vs 58 | range_len = -1 59 | if label in ['初伏', '末伏']: 60 | range_len = 10 61 | elif label == '中伏': 62 | range_len = (days['末伏'] - days['中伏']).days 63 | elif '九' in label: 64 | range_len = 9 65 | offset = (date_obj - sd).days 66 | if 0 <= offset <= range_len - 1: 67 | return f'{label}第{offset + 1}天' 68 | return '' 69 | -------------------------------------------------------------------------------- /borax/capp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/borax/capp/__init__.py -------------------------------------------------------------------------------- /borax/capp/__main__.py: -------------------------------------------------------------------------------- 1 | from borax.capp.borax_calendar_app import start_calendar_app 2 | from borax.capp.festival_creator import start_festival_creator 3 | 4 | if __name__ == '__main__': 5 | import sys 6 | 7 | pro_args = sys.argv[1:] 8 | if 'creator' in pro_args: 9 | start_festival_creator() 10 | else: 11 | start_calendar_app() 12 | -------------------------------------------------------------------------------- /borax/choices.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from typing import Dict 4 | 5 | __all__ = ['Item', 'ConstChoices'] 6 | 7 | 8 | class Item: 9 | _order = 0 10 | 11 | def __init__(self, value, display=None, *, order=None): 12 | self._value = value 13 | if display is None: 14 | self._display = str(value) 15 | else: 16 | self._display = str(display) 17 | if order is None: 18 | Item._order += 1 19 | self.order = Item._order 20 | else: 21 | self.order = order 22 | 23 | @property 24 | def value(self): 25 | return self._value 26 | 27 | @property 28 | def display(self): 29 | return self._display 30 | 31 | @property 32 | def label(self): 33 | return self._display 34 | 35 | def __str__(self): 36 | return f'<{self.__class__.__name__} value={self.value!r} label={self.label!r}>' 37 | 38 | __repr__ = __str__ 39 | 40 | 41 | class ChoicesMetaclass(type): 42 | def __new__(cls, name, bases, attrs): 43 | 44 | fields = {} # {:} 45 | 46 | parents = [b for b in bases if isinstance(b, ChoicesMetaclass)] 47 | for kls in parents: 48 | for field_name in kls.fields: 49 | fields[field_name] = kls.fields[field_name] 50 | 51 | for k, v in attrs.items(): 52 | if k.startswith('_'): 53 | continue 54 | if isinstance(v, Item): 55 | fields[k] = v 56 | elif isinstance(v, (tuple, list)) and len(v) == 2: 57 | fields[k] = Item(v[0], v[1]) 58 | elif isinstance(v, (int, float, str, bytes)): 59 | fields[k] = Item(v) 60 | 61 | fields = OrderedDict(sorted(fields.items(), key=lambda x: x[1].order)) 62 | for field_name, item in fields.items(): 63 | attrs[field_name] = item.value # override the exists attrs __dict__ 64 | 65 | new_cls = super().__new__(cls, name, bases, attrs) 66 | new_cls._fields = fields 67 | return new_cls 68 | 69 | @property 70 | def fields(cls) -> Dict[str, Item]: 71 | return dict(cls._fields) 72 | 73 | @property 74 | def choices(cls) -> list: 75 | return [(item.value, item.label) for _, item in cls.fields.items()] 76 | 77 | @property 78 | def names(cls) -> tuple: 79 | return tuple(cls.fields.keys()) 80 | 81 | @property 82 | def values(cls) -> tuple: 83 | return tuple([value for value, _ in cls.choices]) 84 | 85 | @property 86 | def displays(cls) -> tuple: 87 | return tuple([display for _, display in cls.choices]) 88 | 89 | @property 90 | def labels(cls) -> tuple: 91 | return cls.displays 92 | 93 | @property 94 | def display_lookup(cls) -> dict: 95 | return {value: label for value, label in cls.choices} 96 | 97 | def __contains__(self, item): 98 | return item in self.values 99 | 100 | def __iter__(self): 101 | for item in self.choices: 102 | yield item 103 | 104 | def __len__(self): 105 | return len(self.choices) 106 | 107 | # API 108 | def is_valid(cls, value) -> bool: 109 | return value in cls.display_lookup 110 | 111 | def get_value_display(cls, value, default=None): 112 | return cls.display_lookup.get(value, default) 113 | 114 | 115 | class ConstChoices(metaclass=ChoicesMetaclass): 116 | pass 117 | -------------------------------------------------------------------------------- /borax/constants.py: -------------------------------------------------------------------------------- 1 | EMPTY_VALUES = (None, '', (), []) 2 | 3 | USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.72 Safari/537.36' 4 | 5 | 6 | class DatetimeFormat: 7 | DISPLAY_DATE = '%Y-%m-%d' 8 | DISPLAY_DT = '%Y-%m-%d %H:%M:%S' 9 | SUFFIX_DT = '%Y%m%d%H%M%S' 10 | SUFFIX_DT_UNDERLINE = '%Y_%m_%d_%H_%M_%S' 11 | SUFFIX_DATE = '%Y%m%d' 12 | SUFFIX_DATE_UNDERLINE = '%Y_%m_%d' 13 | -------------------------------------------------------------------------------- /borax/counters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/borax/counters/__init__.py -------------------------------------------------------------------------------- /borax/counters/daily.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | 3 | 4 | class DailyCounter: 5 | # TODO Use Counter 6 | def __init__(self, year, month, raw=None): 7 | self._days = calendar.monthrange(year, month)[1] 8 | self._year = year 9 | self._month = month 10 | if raw: 11 | try: 12 | counter = list(map(int, raw.split(','))) 13 | except (TypeError, ValueError): 14 | counter = [] 15 | if len(counter) == self._days: 16 | self._counter = counter 17 | else: 18 | raise ValueError('Invalid raw data for %d-%d' % (self._year, self._month)) 19 | else: 20 | self._counter = [0] * self._days 21 | 22 | def get_day_counter(self, day): 23 | ii = day - 1 24 | if ii in range(self._days): 25 | return self._counter[ii] 26 | else: 27 | raise ValueError('Invalid day.Interger 1 - %d expected.' % self._days) 28 | 29 | def increase(self, day, step=1): 30 | ii = day - 1 31 | self._counter[ii] += step 32 | 33 | @property 34 | def days(self): 35 | return self._days 36 | 37 | @property 38 | def year(self): 39 | return self._year 40 | 41 | @property 42 | def month(self): 43 | return self._month 44 | 45 | def __iter__(self): 46 | for val in self._counter: 47 | yield val 48 | 49 | def __str__(self): 50 | return ','.join(self._counter) 51 | -------------------------------------------------------------------------------- /borax/counters/serial_pool.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import chain 3 | 4 | from typing import List, Union, Iterable, Generator, Optional, Callable 5 | 6 | # ---------- Custom typing ---------- 7 | ElementsType = List[Union[int, str]] 8 | 9 | 10 | def serial_no_generator(lower: int = 0, upper: int = 10, reused: bool = True, values: Iterable[int] = None) -> \ 11 | Generator[int, None, None]: 12 | values = values or [] 13 | eset = set(filter(lambda x: lower <= x < upper, values)) 14 | if eset: 15 | max_val = max(eset) 16 | if reused: 17 | gen = chain(range(max_val + 1, upper), range(lower, max_val)) 18 | else: 19 | gen = range(max_val + 1, upper) 20 | else: 21 | gen = range(lower, upper) 22 | for ele in gen: 23 | if ele in eset: 24 | continue 25 | yield ele 26 | 27 | 28 | _fmt_re = re.compile(r'\{no(:0(\d+)([bodxX]))?\}') 29 | 30 | b2p_dict = {'b': 2, 'o': 8, 'd': 10, 'x': 16, 'X': 16} 31 | p2b_dict = {2: 'b', 8: 'o', 10: 'd', 16: 'x'} 32 | 33 | 34 | class LabelFormatOpts: 35 | def __init__(self, fmt_str, base=10, digits=2): 36 | 37 | base_char = p2b_dict[base] 38 | data = _fmt_re.findall(fmt_str) 39 | ft = [item[0] for item in data if item[0] != ''] 40 | if ft: 41 | if all(el == ft[0] for el in ft): 42 | base_char = ft[0][-1] 43 | base, digits = b2p_dict.get(base_char), int(ft[0][2:-1]) 44 | else: 45 | raise ValueError(f'{fmt_str} Define different formatter for no variable.') 46 | new_field_fmt = '{{no:0{0}{1}}}'.format(digits, base_char) 47 | 48 | self.origin_fmt = fmt_str 49 | self.normalized_fmt = _fmt_re.sub(new_field_fmt, fmt_str) 50 | 51 | fr_dict = { 52 | 'b': '(?P[01]{{{0}}})'.format(digits), 53 | 'o': '(?P[0-7]{{{0}}})'.format(digits), 54 | 'd': '(?P[0-9]{{{0}}})'.format(digits), 55 | 'x': '(?P[0-9a-f]{{{0}}})'.format(digits), 56 | 'X': '(?P[0-9A-Z]{{{0}}})'.format(digits), 57 | } 58 | 59 | self.parse_re = re.compile(self.normalized_fmt.replace(new_field_fmt, fr_dict[base_char])) 60 | self.base = base 61 | self.digits = digits 62 | self.base_char = base_char 63 | 64 | def value2label(self, value: int) -> str: 65 | return self.normalized_fmt.format(no=value) 66 | 67 | def label2value(self, label: str) -> int: 68 | m = self.parse_re.match(label) 69 | if m: 70 | return int(m.group('no'), base=self.base) 71 | raise ValueError(f'Error Value {label}') 72 | 73 | 74 | class SerialElement: 75 | __slots__ = ['value', 'label'] 76 | 77 | def __init__(self, value, label): 78 | self.value = value 79 | self.label = label 80 | 81 | 82 | class SerialNoPool: 83 | def __init__(self, lower: int = None, upper: int = None, base: int = 0, digits: int = 0, 84 | label_fmt: Optional[str] = None): 85 | 86 | if label_fmt is None: 87 | self._opts = None 88 | else: 89 | base = base or 10 90 | digits = digits or 2 91 | self._opts = LabelFormatOpts(label_fmt, base, digits) 92 | base = self._opts.base 93 | digits = self._opts.digits 94 | 95 | if lower is not None and lower < 0: 96 | raise ValueError(f'lower(={lower}) must be >= 0.') 97 | if upper is not None and upper <= 0: 98 | raise ValueError(f'upper(={upper}) must be >= 0.') 99 | s_set = base and digits 100 | t_set = lower is not None and upper is not None 101 | 102 | if t_set: 103 | self._lower, self._upper = lower, upper 104 | if s_set: 105 | cl, cu = 0, base ** digits 106 | if not (lower >= cl and upper <= cu): 107 | raise ValueError(f'The lower-upper [{lower},{upper}) is not in [{cl},{cu})') 108 | else: 109 | if s_set: 110 | self._lower, self._upper = 0, base ** digits 111 | else: 112 | self._lower, self._upper = 0, 100 113 | 114 | self._values = set() 115 | self._source = None 116 | 117 | # ---------- Pool Attributes ---------- 118 | 119 | @property 120 | def lower(self): 121 | return self._lower 122 | 123 | @property 124 | def upper(self): 125 | return self._upper 126 | 127 | # ---------- Data API ---------- 128 | 129 | def set_elements(self, elements: ElementsType) -> 'SerialNoPool': 130 | self._values = set() 131 | self.add_elements(elements) 132 | return self 133 | 134 | def set_source(self, source: Callable[[], ElementsType]) -> 'SerialNoPool': 135 | self._source = source 136 | return self 137 | 138 | def add_elements(self, elements: ElementsType) -> 'SerialNoPool': 139 | values = self._elements2values(elements) 140 | for v in values: 141 | self._values.add(v) 142 | return self 143 | 144 | def remove_elements(self, elements: ElementsType) -> 'SerialNoPool': 145 | values = self._elements2values(elements) 146 | for v in values: 147 | self._values.remove(v) 148 | return self 149 | 150 | def _elements2values(self, elements: ElementsType) -> List[int]: 151 | values = [] # type: List[int] 152 | for ele in elements: 153 | if isinstance(ele, int): 154 | value = ele 155 | elif isinstance(ele, str): 156 | value = self._opts.label2value(ele) 157 | else: 158 | raise TypeError(f'Invalid element {ele}:unsupported type.') 159 | if self._lower <= value < self._upper: 160 | values.append(value) 161 | else: 162 | raise ValueError(f'Invalid element {ele}: range error') 163 | return values 164 | 165 | # ---------- Generate API ---------- 166 | 167 | def get_next_generator(self) -> Generator[SerialElement, None, None]: 168 | """ 169 | This is the low-level method. 170 | :return: 171 | """ 172 | if self._source is not None: 173 | elements = self._source() 174 | self.set_elements(elements) 175 | value_gen = serial_no_generator(lower=self._lower, upper=self._upper, values=self._values) 176 | for value in value_gen: 177 | if self._opts: 178 | label = self._opts.value2label(value) 179 | else: 180 | label = None 181 | yield SerialElement(value, label) 182 | 183 | def generate_values(self, num=1) -> List[int]: 184 | return [se.value for se in self.get_next_generator()][:num] 185 | 186 | def generate_labels(self, num=1) -> List[str]: 187 | if self._opts is None: 188 | raise TypeError('The operation generate_labels is not allowed when label_fmt is not set.') 189 | return [se.label for se in self.get_next_generator()][:num] 190 | 191 | def generate(self, num=1) -> List[str]: 192 | return self.generate_labels(num) 193 | -------------------------------------------------------------------------------- /borax/datasets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/borax/datasets/__init__.py -------------------------------------------------------------------------------- /borax/datasets/fetch.py: -------------------------------------------------------------------------------- 1 | """ 2 | fetch is a enhance module with fetch. And adjust the parameter order of calling to fit the habit. 3 | """ 4 | from functools import partial 5 | from itertools import tee 6 | 7 | __all__ = ['Empty', 'bget', 'fetch', 'ifetch', 'fetch_single', 'ifetch_multiple', 'ifetch_single', 'fetch_as_dict'] 8 | 9 | 10 | class Empty: 11 | pass 12 | 13 | 14 | EMPTY = Empty() 15 | 16 | 17 | def bget(obj, key, default=Empty): 18 | try: 19 | return obj[key] 20 | except (TypeError, KeyError): 21 | pass 22 | try: 23 | return getattr(obj, key) 24 | except (AttributeError, TypeError): 25 | pass 26 | 27 | if default is not EMPTY: 28 | return default 29 | 30 | raise ValueError(f'Item {obj!r} has no attr or key for {key!r}') 31 | 32 | 33 | def ifetch_single(iterable, key, default=EMPTY, getter=None): 34 | """ 35 | getter() g(item, key):pass 36 | """ 37 | 38 | def _getter(item): 39 | if getter: 40 | custom_getter = partial(getter, key=key) 41 | return custom_getter(item) 42 | else: 43 | return partial(bget, key=key, default=default)(item) 44 | 45 | return map(_getter, iterable) 46 | 47 | 48 | def fetch_single(iterable, key, default=EMPTY, getter=None): 49 | return list(ifetch_single(iterable, key, default=default, getter=getter)) 50 | 51 | 52 | def ifetch_multiple(iterable, *keys, defaults=None, getter=None): 53 | defaults = defaults or {} 54 | if len(keys) > 1: 55 | iters = tee(iterable, len(keys)) 56 | else: 57 | iters = (iterable,) 58 | iters = [ifetch_single(it, key, default=defaults.get(key, EMPTY), getter=getter) for it, key in zip(iters, keys)] 59 | return iters 60 | 61 | 62 | def ifetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None): 63 | if len(keys) > 0: 64 | keys = (key,) + keys 65 | return map(list, ifetch_multiple(iterable, *keys, defaults=defaults, getter=getter)) 66 | else: 67 | return ifetch_single(iterable, key, default=default, getter=getter) 68 | 69 | 70 | def fetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None): 71 | """Pick values from each field. 72 | 73 | >>> persons = [{'id': 1, 'name': 'Alice', 'age': 30},{'id': 2, 'name': 'John', 'age': 24}] 74 | >>> fetch(persons, 'name') 75 | ['Alice', 'John'] 76 | 77 | >>> data = [[1, 2, 3, 4,], [11,12,13,14],[21,22,23,24]] 78 | >>> ones, threes = fetch(data, 0, 2) 79 | >>> ones 80 | [1, 11, 21] 81 | >>> threes 82 | [3, 13, 23] 83 | """ 84 | return list(ifetch(iterable, key, *keys, default=default, defaults=defaults, getter=getter)) 85 | 86 | 87 | def fetch_as_dict(data, key_field, value_value): 88 | """Build a dict for a iterable data. 89 | 90 | >>> persons = [{'id': 1, 'name': 'Alice', 'age': 30},{'id': 2, 'name': 'John', 'age': 24}] 91 | >>> fetch_as_dict(persons, 'name', 'age') 92 | {'Alice': 30, 'John': 24} 93 | """ 94 | return {bget(item, key_field): bget(item, value_value) for item in data} 95 | -------------------------------------------------------------------------------- /borax/datasets/join_.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import copy 3 | 4 | __all__ = ['join_one', 'join'] 5 | 6 | 7 | def join_one(ldata, rdata, on, select_as, default=None): 8 | if isinstance(rdata, (list, tuple)): 9 | rdata = dict(rdata) 10 | if not isinstance(rdata, dict): 11 | raise TypeError("Unsupported Type for values param.") 12 | 13 | if isinstance(on, str): 14 | lic = operator.itemgetter(on) 15 | elif callable(on): 16 | lic = on 17 | else: 18 | raise TypeError('str or callable only supported for on param. ') 19 | 20 | for litem in ldata: 21 | if not (isinstance(on, str) and on not in litem): 22 | lv = lic(litem) 23 | rvv = rdata.get(lv, default) 24 | litem[select_as] = rvv 25 | return ldata 26 | 27 | 28 | CLAUSE_SINGLE_TYPES = (str, tuple) 29 | 30 | 31 | class OnClause(tuple): 32 | def __new__(self, lkey, rkey=None): 33 | rkey = rkey or lkey 34 | return tuple.__new__(OnClause, (lkey, rkey)) 35 | 36 | @classmethod 37 | def from_val(cls, val): 38 | cm = val.__class__.__name__ 39 | if cm == "OnClause": 40 | return val 41 | elif cm == "str": 42 | return cls(val, val) 43 | elif cm == "tuple": 44 | return cls(*val[:2]) 45 | else: 46 | raise TypeError(f"Cannot build OnClause from a {cm} object.") 47 | 48 | 49 | class SelectClause(tuple): 50 | def __new__(self, rkey, lkey=None, default=None): 51 | lkey = lkey or rkey 52 | return tuple().__new__(SelectClause, (rkey, lkey, default)) 53 | 54 | @classmethod 55 | def from_val(cls, val): 56 | cm = val.__class__.__name__ 57 | if cm == "SelectClause": 58 | return val 59 | elif cm == "str": 60 | return cls(val, val, None) 61 | elif cm == "tuple": 62 | return cls(*val[:3]) 63 | else: 64 | raise TypeError(f"Cannot build SelectClause from a {cm} object.") 65 | 66 | 67 | OC = OnClause 68 | SC = SelectClause 69 | 70 | 71 | def _pick_data(_item, _sfs): 72 | result = {} 73 | for rk, lk, defv in _sfs: 74 | result[lk] = _item.get(rk, defv) 75 | return result 76 | 77 | 78 | def join(ldata, rdata, on, select_as, defaults=None): 79 | if isinstance(on, CLAUSE_SINGLE_TYPES): 80 | on = [on] 81 | if isinstance(on, list): 82 | lfields, rfields = zip(*list(map(OnClause.from_val, on))) 83 | 84 | def on_callback(_li, _ri): 85 | for _lf, _rf in zip(lfields, rfields): 86 | if _lf in _li and _rf in _ri: 87 | if _li[_lf] != _ri[_rf]: 88 | return False 89 | else: 90 | return False 91 | else: 92 | return True 93 | 94 | elif callable(on): 95 | on_callback = on 96 | else: 97 | raise TypeError('str or callable only supported for on param. ') 98 | 99 | if isinstance(select_as, CLAUSE_SINGLE_TYPES): 100 | select_as = [select_as] 101 | sf_list = list(map(SelectClause.from_val, select_as)) 102 | 103 | defaults = defaults or {} 104 | for litem in ldata: 105 | for ritem in rdata: 106 | if on_callback(litem, ritem): 107 | _ri = ritem 108 | break 109 | else: 110 | _ri = defaults 111 | litem.update(_pick_data(_ri, sf_list)) 112 | return ldata 113 | 114 | 115 | def deep_join_one(ldata, rdata, on, select_as, default=None): 116 | ldata = copy.deepcopy(ldata) 117 | return join_one(ldata, rdata, on, select_as, default=default) 118 | 119 | 120 | def deep_join(ldata, rdata, on, select_as, defaults=None): 121 | ldata = copy.deepcopy(ldata) 122 | return join(ldata, rdata, on, select_as, defaults) 123 | -------------------------------------------------------------------------------- /borax/devtools.py: -------------------------------------------------------------------------------- 1 | import time 2 | from collections import defaultdict, namedtuple 3 | from contextlib import contextmanager 4 | 5 | from typing import Dict 6 | 7 | TagMeasureResult = namedtuple('TagMeasureResult', 'name total count avg') 8 | 9 | 10 | class RuntimeMeasurer: 11 | """A time measurer for a program.""" 12 | 13 | def __init__(self): 14 | self._data = defaultdict(list) 15 | self._start_time_dict = {} 16 | 17 | def start(self, *tags: str) -> 'RuntimeMeasurer': 18 | st = time.time() 19 | for _tag in tags: 20 | self._start_time_dict[_tag] = st 21 | return self 22 | 23 | def end(self, *tags: str) -> 'RuntimeMeasurer': 24 | et = time.time() 25 | for _tag in tags: 26 | if _tag in self._start_time_dict: 27 | self._data[_tag].append(et - self._start_time_dict[_tag]) 28 | return self 29 | 30 | @contextmanager 31 | def measure(self, *tags: str): 32 | try: 33 | self.start(*tags) 34 | yield 35 | finally: 36 | self.end(*tags) 37 | 38 | def get_measure_result(self) -> Dict[str, TagMeasureResult]: 39 | result = {} 40 | for tag, values in self._data.items(): 41 | tv = sum(values) 42 | cv = len(values) 43 | result[tag] = TagMeasureResult(tag, tv, cv, tv / cv) 44 | return result 45 | 46 | def print_(self): 47 | """Print statistics data for all tags.""" 48 | data = self.get_measure_result() 49 | print("{:>8} {:>10} {:>10} {:>10}".format('name', 'total', 'count', 'avg')) 50 | for v in data.values(): 51 | name, total, count, avg = v 52 | print(f"{name:>8} {total:>.8f} {count:>10} {avg:>.8f}") 53 | -------------------------------------------------------------------------------- /borax/htmls.py: -------------------------------------------------------------------------------- 1 | import html 2 | from typing import Dict, List, Union 3 | 4 | 5 | def _escape(s): 6 | if hasattr(s, '__html__'): 7 | return s.__html__() 8 | return HTMLString(html.escape(str(s))) 9 | 10 | 11 | class HTMLString(str): 12 | """Implement __html__ protocol for str class.inspired by SafeData in django and Markup in jinja2. 13 | """ 14 | 15 | def __new__(cls, base='', encoding=None, errors='strict'): 16 | if hasattr(base, '__html__'): 17 | base = base.__html__() 18 | if encoding is None: 19 | return str.__new__(cls, base) 20 | return str.__new__(cls, base, encoding, errors) 21 | 22 | def __html__(self): 23 | return self 24 | 25 | @classmethod 26 | def escape(cls, s): 27 | rv = _escape(s) 28 | if rv.__class__ is not cls: 29 | return cls(rv) 30 | return rv 31 | 32 | 33 | def html_params(**kwargs) -> str: 34 | params = [] 35 | for k, v in kwargs.items(): 36 | if k in ('class_', 'for_', 'id_'): 37 | k = k[:-1] 38 | elif k.startswith('data_'): 39 | k = k.replace('_', '-') 40 | if v is True: 41 | params.append(k) 42 | elif v is False: 43 | pass 44 | elif isinstance(v, dict): 45 | v = {k_: v_ for k_, v_ in v.items() if v_ not in (None, '', [], ())} 46 | vs = ''.join([f'{k}:{v};' for k, v in v.items()]) 47 | params.append(f'{k}="{vs}"') 48 | elif isinstance(v, list): 49 | vs = ' '.join(map(str, v)) 50 | params.append(f'{k}="{vs}"') 51 | else: 52 | vs = html.escape(str(v), quote=True) 53 | params.append(f'{k}="{vs}"') 54 | return ' '.join(params) 55 | 56 | 57 | SINGLE_TAGS = ('br', 'hr', 'img', 'input', 'param', 'meta', 'link') 58 | 59 | 60 | def html_tag(tag_name: str, content: str = None, 61 | *, id_: str = None, style: Dict = None, class_: Union[List, str, None] = None, style_width: str = None, 62 | style_height: str = None, **kwargs) -> HTMLString: 63 | """Generate a HTML-safe string for a html element.""" 64 | kw = {} 65 | if id_: 66 | kw['id_'] = id_ 67 | style = style or {} 68 | if style_width is not None: 69 | style.update({'width': style_width}) 70 | if style_height is not None: 71 | style.update({'height': style_height}) 72 | if style: 73 | kw['style'] = style 74 | class_ = class_ or [] 75 | if class_: 76 | kw['class_'] = class_ 77 | kw.update(kwargs) 78 | if tag_name in SINGLE_TAGS: 79 | return HTMLString(f'<{tag_name} {html_params(**kw)}>') 80 | else: 81 | content = content or '' 82 | return HTMLString(f'<{tag_name} {html_params(**kw)}>{content}') 83 | -------------------------------------------------------------------------------- /borax/numbers.py: -------------------------------------------------------------------------------- 1 | import re 2 | from decimal import Decimal 3 | 4 | from typing import Union 5 | 6 | __all__ = ['MAX_VALUE_LIMIT', 'LOWER_DIGITS', 'UPPER_DIGITS', 'ChineseNumbers', 'FinanceNumbers'] 7 | 8 | MAX_VALUE_LIMIT = 1000000000000 # 10^12 9 | 10 | LOWER_UNITS = '千百十亿千百十万千百十_' 11 | LOWER_DIGITS = '零一二三四五六七八九' 12 | 13 | UPPER_UNITS = '仟佰拾亿仟佰拾万仟佰拾_' 14 | UPPER_DIGITS = '零壹贰叁肆伍陆柒捌玖' 15 | 16 | 17 | class ChineseNumbers: 18 | RULES = [ 19 | (r'一十', '十'), 20 | (r'零[千百十]', '零'), 21 | (r'零{2,}', '零'), 22 | (r'零([亿|万])', r'\g<1>'), 23 | (r'亿零{0,3}万', '亿'), 24 | (r'零?_', ''), 25 | ] 26 | 27 | @staticmethod 28 | def measure_number(num: Union[int, str], upper: bool = False) -> str: 29 | """将数字转化为计量大/小写的中文数字,数字0的中文形式为“零”。 30 | 31 | >>> ChineseNumbers.measure_number(11) 32 | '十一' 33 | >>> ChineseNumbers.measure_number(204, True) 34 | '贰佰零肆' 35 | """ 36 | if isinstance(num, str): 37 | _n = int(num) 38 | else: 39 | _n = num 40 | if _n < 0 or _n >= MAX_VALUE_LIMIT: 41 | raise ValueError('Out of range') 42 | num_str = str(num) 43 | capital_str = ''.join([LOWER_DIGITS[int(i)] for i in num_str]) 44 | s_units = LOWER_UNITS[len(LOWER_UNITS) - len(num_str):] 45 | 46 | o = ''.join(f'{u}{d}' for u, d in zip(capital_str, s_units)) 47 | for p, d in ChineseNumbers.RULES: 48 | o = re.sub(p, d, o) 49 | if 10 <= _n < 20: 50 | o.replace('一十', '十') 51 | if upper: 52 | for _ld, _ud in zip(LOWER_DIGITS + LOWER_UNITS[:3], UPPER_DIGITS + UPPER_UNITS[:3]): 53 | o = o.replace(_ld, _ud) 54 | return o 55 | 56 | @staticmethod 57 | def order_number(num: Union[int, str], upper: bool = False) -> str: 58 | """将数字转化为编号大/小写的中文数字,数字0的中文形式为“〇”。 59 | 60 | >>> ChineseNumbers.order_number(1056) 61 | '一千〇五十六' 62 | """ 63 | val = ChineseNumbers.measure_number(num, upper) 64 | ns = val.replace('零', '〇') 65 | return ns 66 | 67 | @staticmethod 68 | def to_chinese_number(num: Union[int, str], upper: bool = False, order: bool = False) -> str: 69 | """ 70 | 71 | >>> ChineseNumbers.to_chinese_number(100000000) 72 | '一亿' 73 | >>> ChineseNumbers.to_chinese_number(204, upper=True) 74 | '贰佰零肆' 75 | >>> ChineseNumbers.to_chinese_number(204, upper=True, order=True) 76 | '贰佰〇肆' 77 | """ 78 | if order: 79 | return ChineseNumbers.order_number(num, upper) 80 | else: 81 | return ChineseNumbers.measure_number(num, upper) 82 | 83 | 84 | class FinanceNumbers: 85 | RULES = [ 86 | (r'零角零分$', '整'), 87 | (r'零[仟佰拾]', '零'), 88 | (r'零{2,}', '零'), 89 | (r'零([亿|万])', r'\g<1>'), 90 | (r'零+元', '元'), 91 | (r'亿零{0,3}万', '亿'), 92 | (r'^元', '零元') 93 | ] 94 | 95 | @staticmethod 96 | def to_capital_str(num: Union[int, float, Decimal, str]) -> str: 97 | """Convert a int or float object to finance numeric string. 98 | 99 | >>> FinanceNumbers.to_capital_str(100000000) 100 | '壹亿元整' 101 | >>> FinanceNumbers.to_capital_str(80.02) 102 | '捌拾元零角贰分' 103 | >>> import decimal 104 | >>> FinanceNumbers.to_capital_str(decimal.Decimal(4.50)) 105 | '肆元伍角零分' 106 | """ 107 | units = UPPER_UNITS[:-1] + '元角分' 108 | if isinstance(num, str): 109 | _n = Decimal(num) 110 | else: 111 | _n = num 112 | if _n < 0 or _n >= MAX_VALUE_LIMIT: 113 | raise ValueError('Out of range') 114 | 115 | num_str = str(num) + '00' 116 | dot_pos = num_str.find('.') 117 | if dot_pos > -1: 118 | num_str = num_str[:dot_pos] + num_str[dot_pos + 1:dot_pos + 3] 119 | capital_str = ''.join([UPPER_DIGITS[int(i)] for i in num_str]) 120 | s_units = units[len(units) - len(num_str):] 121 | 122 | o = ''.join(f'{u}{d}' for u, d in zip(capital_str, s_units)) 123 | for p, d in FinanceNumbers.RULES: 124 | o = re.sub(p, d, o) 125 | 126 | return o 127 | -------------------------------------------------------------------------------- /borax/patterns/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/borax/patterns/__init__.py -------------------------------------------------------------------------------- /borax/patterns/lazy.py: -------------------------------------------------------------------------------- 1 | """ 2 | A Lazy Creator for a Object. 3 | """ 4 | 5 | import operator 6 | 7 | __all__ = ['LazyObject'] 8 | 9 | EMPTY = object() 10 | 11 | 12 | def proxy_method(func): 13 | def inner(self, *args): 14 | if self._wrapped is EMPTY: 15 | self._setup() 16 | return func(self._wrapped, *args) 17 | 18 | return inner 19 | 20 | 21 | class LazyObject: 22 | _wrapped = None 23 | 24 | def __init__(self, func, args=None, kwargs=None): 25 | self.__dict__['_setupfunc'] = func 26 | self.__dict__['_args'] = args or [] 27 | self.__dict__['_kwargs'] = kwargs or {} 28 | self._wrapped = EMPTY 29 | 30 | def _setup(self): 31 | self._wrapped = self._setupfunc(*self._args, **self._kwargs) 32 | 33 | __getattr__ = proxy_method(getattr) 34 | 35 | def __setattr__(self, key, value): 36 | if key == '_wrapped': 37 | self.__dict__['_wrapped'] = value 38 | else: 39 | if self._wrapped is EMPTY: 40 | self._setup() 41 | setattr(self._wrapped, key, value) 42 | 43 | def __delattr__(self, name): 44 | if name == "_wrapped": 45 | raise TypeError("can't delete _wrapped.") 46 | if self._wrapped is EMPTY: 47 | self._setup() 48 | delattr(self._wrapped, name) 49 | 50 | __getitem__ = proxy_method(operator.getitem) 51 | __class__ = property(proxy_method(operator.attrgetter("__class__"))) 52 | __eq__ = proxy_method(operator.eq) 53 | __ne__ = proxy_method(operator.ne) 54 | __hash__ = proxy_method(hash) 55 | __bytes__ = proxy_method(bytes) 56 | __str__ = proxy_method(str) 57 | __bool__ = proxy_method(bool) 58 | -------------------------------------------------------------------------------- /borax/patterns/proxy.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://github.com/coleifer/peewee/blob/3.8.2/peewee.py 3 | """ 4 | 5 | 6 | class Proxy: 7 | """ 8 | Create a proxy or placeholder for another object. 9 | """ 10 | __slots__ = ('obj', '_callbacks') 11 | 12 | def __init__(self): 13 | self._callbacks = [] 14 | self.initialize(None) 15 | 16 | def initialize(self, obj): 17 | self.obj = obj 18 | for callback in self._callbacks: 19 | callback(obj) 20 | 21 | def attach_callback(self, callback): 22 | self._callbacks.append(callback) 23 | return callback 24 | 25 | def passthrough(method): 26 | def inner(self, *args, **kwargs): 27 | if self.obj is None: 28 | raise AttributeError('Cannot use uninitialized Proxy.') 29 | return getattr(self.obj, method)(*args, **kwargs) 30 | 31 | return inner 32 | 33 | # Allow proxy to be used as a context-manager. 34 | __enter__ = passthrough('__enter__') 35 | __exit__ = passthrough('__exit__') 36 | 37 | def __getattr__(self, attr): 38 | if self.obj is None: 39 | raise AttributeError('Cannot use uninitialized Proxy.') 40 | return getattr(self.obj, attr) 41 | 42 | def __setattr__(self, attr, value): 43 | if attr not in self.__slots__: 44 | raise AttributeError('Cannot set attribute on proxy.') 45 | return super().__setattr__(attr, value) 46 | -------------------------------------------------------------------------------- /borax/patterns/singleton.py: -------------------------------------------------------------------------------- 1 | class MetaSingleton(type): 2 | def __init__(cls, *args): 3 | type.__init__(cls, *args) 4 | cls.instance = None 5 | 6 | def __call__(cls, *args, **kwargs): 7 | if not cls.instance: 8 | cls.instance = type.__call__(cls, *args, **kwargs) 9 | return cls.instance 10 | -------------------------------------------------------------------------------- /borax/serialize/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/borax/serialize/__init__.py -------------------------------------------------------------------------------- /borax/serialize/cjson.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime, date 3 | from functools import singledispatch 4 | 5 | __all__ = ['encode_object', 'encoder', 'dumps', 'dump', 'CJSONEncoder'] 6 | 7 | 8 | def encode_object(obj): 9 | if hasattr(obj, '__json__'): 10 | return obj.__json__() 11 | raise TypeError(f'Type {obj.__class__.__name__} is not JSON serializable') 12 | 13 | 14 | def _unregister(self, cls): 15 | self.register(cls, encode_object) 16 | 17 | 18 | encoder = singledispatch(encode_object) 19 | encoder.unregister = _unregister.__get__(encoder) # see more detail on https://stackoverflow.com/a/28060251 20 | 21 | 22 | def dumps(obj, **kwargs): 23 | return json.dumps(obj, default=encoder, **kwargs) 24 | 25 | 26 | def dump(obj, fp, **kwargs): 27 | return json.dump(obj, fp, default=encoder, **kwargs) 28 | 29 | 30 | encoder.register(datetime, lambda obj: obj.strftime('%Y-%m-%d %H:%M:%S')) 31 | encoder.register(date, lambda obj: obj.strftime('%Y-%m-%d')) 32 | 33 | 34 | class CJSONEncoder(json.JSONEncoder): 35 | def default(self, o): 36 | return encoder(o) 37 | -------------------------------------------------------------------------------- /borax/strings.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def camel2snake(s: str) -> str: 5 | """Convert camel string to snake string. 6 | 7 | >>> camel2snake('Act') 8 | 'act' 9 | >>> camel2snake('SnakeString') 10 | 'snake_string' 11 | """ 12 | camel_to_snake_regex = r'((?<=[a-z0-9])[A-Z]|(?!^)(? str: 17 | """Convert snake string to camel string. 18 | 19 | >>> snake2camel('snake_string') 20 | 'SnakeString' 21 | >>> snake2camel('act') 22 | 'Act' 23 | """ 24 | snake_to_camel_regex = r"(?:^|_)(.)" 25 | return re.sub(snake_to_camel_regex, lambda m: m.group(1).upper(), s) 26 | 27 | 28 | def get_percentage_display(value, places=2): 29 | fmt = '{:. %}'.replace(' ', str(places)) 30 | return fmt.format(value) 31 | 32 | 33 | class FileEndingUtil: 34 | WINDOWS_LINE_ENDING = b'\r\n' 35 | LINUX_LINE_ENDING = b'\n' 36 | 37 | @staticmethod 38 | def windows2linux(content: bytes) -> bytes: 39 | assert isinstance(content, bytes) 40 | return content.replace(FileEndingUtil.WINDOWS_LINE_ENDING, FileEndingUtil.LINUX_LINE_ENDING) 41 | 42 | @staticmethod 43 | def linux2windows(content: bytes) -> bytes: 44 | return content.replace(FileEndingUtil.LINUX_LINE_ENDING, FileEndingUtil.WINDOWS_LINE_ENDING) 45 | 46 | @staticmethod 47 | def convert_to_linux_style_file(file_path): 48 | with open(file_path, 'rb') as f: 49 | content = f.read() 50 | content = FileEndingUtil.windows2linux(content) 51 | with open(file_path, 'wb') as f: 52 | f.write(content) 53 | 54 | @staticmethod 55 | def convert_to_windows_style_file(file_path): 56 | with open(file_path, 'rb') as f: 57 | content = f.read() 58 | content = FileEndingUtil.linux2windows(content) 59 | with open(file_path, 'wb') as f: 60 | f.write(content) 61 | -------------------------------------------------------------------------------- /borax/structures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/borax/structures/__init__.py -------------------------------------------------------------------------------- /borax/structures/dictionary.py: -------------------------------------------------------------------------------- 1 | class AttributeDict(dict): 2 | def __getattr__(self, key): 3 | try: 4 | return self[key] 5 | except KeyError as e: 6 | # to conform with __getattr__ spec 7 | raise AttributeError(key) from e 8 | 9 | def __setattr__(self, key, value): 10 | self[key] = value 11 | 12 | 13 | AD = AttributeDict 14 | -------------------------------------------------------------------------------- /borax/structures/percentage.py: -------------------------------------------------------------------------------- 1 | def format_percentage(numerator: int, denominator: int, *, places: int = 2, null_val: str = '-') -> str: 2 | if denominator == 0: 3 | return null_val 4 | percent_fmt = '{0:. %}'.replace(' ', str(places)) 5 | val = round(numerator / denominator, places + 2) 6 | return percent_fmt.format(val) 7 | 8 | 9 | class Percentage: 10 | """A object representing a percentage. 11 | 12 | >>> p = Percentage(34) 13 | >>> p.completed 14 | 34 15 | >>> p.percent_display 16 | '34.00%' 17 | >>> p.fraction_display 18 | '34 / 100' 19 | """ 20 | 21 | def __init__(self, *, total: int = 100, completed: int = 0, places: int = 2, 22 | display_fmt: str = '{completed} / {total}', null_val: str = '-'): 23 | self.total = total 24 | self.completed = completed 25 | self._display_fmt = display_fmt 26 | self._places = places 27 | # string.format will fails here 28 | self._null_val = null_val 29 | 30 | def increase(self, value: int = 1) -> None: 31 | self.completed += value 32 | 33 | def decrease(self, value: int = 1) -> None: 34 | self.completed -= value 35 | 36 | @property 37 | def percent(self) -> float: 38 | if self.total == 0: 39 | return 0 40 | else: 41 | return round(self.completed / self.total, self._places + 2) 42 | 43 | @property 44 | def percent_display(self) -> str: 45 | """percent format string like '12.34%' """ 46 | return format_percentage(self.completed, self.total, places=self._places, null_val=self._null_val) 47 | 48 | @property 49 | def fraction_display(self): 50 | """return a fraction like '34 / 100'""" 51 | return self._display_fmt.format(completed=self.completed, total=self.total) 52 | 53 | @property 54 | def display(self) -> str: 55 | """old alias name for fraction_display'""" 56 | return self.fraction_display 57 | 58 | def as_dict(self, prefix: str = '') -> dict: 59 | return { 60 | prefix + 'total': self.total, 61 | prefix + 'completed': self.completed, 62 | prefix + 'percent': self.percent, 63 | prefix + 'percent_display': self.percent_display, 64 | prefix + 'display': self.display 65 | } 66 | 67 | def generate(self, char_total: int = 100) -> str: 68 | char_completed = int(self.percent * char_total) 69 | return '|{0}{1}| {2:.2%}'.format( 70 | '▇' * char_completed, 71 | '░' * (char_total - char_completed), 72 | self.percent * 100 73 | ) 74 | 75 | def __str__(self): 76 | return f'' 77 | -------------------------------------------------------------------------------- /borax/structures/tree.py: -------------------------------------------------------------------------------- 1 | __all__ = ['pll2cnl'] 2 | 3 | 4 | def _get(item, key): 5 | try: 6 | return getattr(item, key) 7 | except AttributeError: 8 | pass 9 | try: 10 | return item[key] 11 | except KeyError: 12 | pass 13 | raise ValueError(f'Item {item!r} has no attr or key for {key!r}') 14 | 15 | 16 | def _parse_extra_data(item, *, flat_fields, extra_fields, extra_key, trs_fields=None): 17 | if isinstance(item, dict) and not flat_fields and not extra_key: 18 | trs_fields = trs_fields or [] 19 | return {k: v for k, v in item.items() if k not in trs_fields} 20 | flat_data = {f: _get(item, f) for f in flat_fields} 21 | if extra_key: 22 | extra_data = {f: _get(item, f) for f in extra_fields} 23 | return { 24 | **flat_data, 25 | **{extra_key: extra_data} 26 | } 27 | return flat_data 28 | 29 | 30 | def pll2cnl( 31 | nodelist, 32 | *, 33 | # source Data Settings 34 | id_field='id', 35 | parent_field='parent', 36 | root_value=None, 37 | # Generated Data Settings 38 | children_field='children', 39 | flat_fields=None, 40 | extra_fields=None, 41 | extra_key=None 42 | 43 | ): 44 | # Prepare and check params 45 | flat_fields = flat_fields or [] 46 | extra_fields = extra_fields or [] 47 | 48 | for f in flat_fields: 49 | if f in (id_field, parent_field, children_field): 50 | raise ValueError(f'Invalid field name: {f}') 51 | if extra_key and extra_key in (id_field, parent_field, children_field): 52 | raise ValueError('extra_key can not be empty when flat is set to False.') 53 | 54 | # Start build 55 | forest = [] 56 | nodes = {} 57 | 58 | for item in nodelist: 59 | 60 | node_id = _get(item, id_field) 61 | parent_id = _get(item, parent_field) 62 | node = nodes.get(node_id, {id_field: node_id}) 63 | kwargs_data = _parse_extra_data( 64 | item, 65 | flat_fields=flat_fields, 66 | extra_fields=extra_fields, 67 | extra_key=extra_key, 68 | trs_fields=(id_field, parent_field) 69 | ) 70 | node.update(kwargs_data) 71 | nodes[node_id] = node 72 | 73 | if parent_id == root_value: 74 | # add node to forrest 75 | forest.append(node) 76 | else: 77 | # create parent node if necessary 78 | if parent_id in nodes: 79 | parent = nodes[parent_id] 80 | 81 | else: 82 | parent = {id_field: parent_id} 83 | nodes[parent_id] = parent 84 | # create children if necessary 85 | if children_field not in parent: 86 | parent[children_field] = [] 87 | # add node to children of parent 88 | parent[children_field].append(node) 89 | 90 | return forest 91 | -------------------------------------------------------------------------------- /borax/system.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from datetime import datetime 4 | 5 | from borax.constants import DatetimeFormat 6 | 7 | 8 | def load_class(s): 9 | """Import a class 10 | :param s: the full path of the class 11 | :return: 12 | """ 13 | path, class_ = s.rsplit('.', 1) 14 | __import__(path) 15 | mod = sys.modules[path] 16 | return getattr(mod, class_) 17 | 18 | 19 | load_object = load_class # Only a alias name. 20 | 21 | 22 | def check_path_variables(execute_filename: str) -> bool: 23 | try: 24 | user_paths = os.environ['PYTHONPATH'].split(os.pathsep) 25 | except KeyError: 26 | user_paths = [] 27 | for item in user_paths: 28 | if os.path.exists(os.path.join(item, execute_filename)): 29 | return True 30 | os_paths_list = os.environ['PATH'].split(';') 31 | for item in os_paths_list: 32 | if os.path.exists(os.path.join(item, execute_filename)): 33 | return True 34 | return False 35 | 36 | 37 | # These constants has been deprecated. 38 | SUFFIX_DT = '%Y%m%d%H%M%S' 39 | SUFFIX_DT_UNDERLINE = '%Y_%m_%d_%H_%M_%S' 40 | SUFFIX_DATE = '%Y%m%d' 41 | SUFFIX_DATE_UNDERLINE = '%Y_%m_%d' 42 | 43 | 44 | def rotate_filename(filename: str, time_fmt: str = DatetimeFormat.SUFFIX_DT, sep: str = '_', now=None, **kwargs) -> str: 45 | """ Rotate filename or filepath with datetime string as suffix. 46 | :param filename: 47 | :param time_fmt: 48 | :param sep: 49 | :param now: 50 | :param kwargs: 51 | :return: 52 | """ 53 | now = now or datetime.now() 54 | kwargs.update({'now': now}) 55 | actual_path = filename.format(**kwargs) 56 | s1, s2 = os.path.splitext(actual_path) 57 | return ''.join([s1, sep, now.strftime(time_fmt), s2]) 58 | -------------------------------------------------------------------------------- /borax/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/borax/ui/__init__.py -------------------------------------------------------------------------------- /borax/ui/aiotk.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import asyncio 3 | 4 | __all__ = ['run_loop'] 5 | 6 | 7 | async def run_loop(app, interval=0.05): 8 | try: 9 | while True: 10 | app.update() 11 | await asyncio.sleep(interval) 12 | except tk.TclError as e: 13 | if "application has been destroyed" not in e.args[0]: 14 | raise 15 | -------------------------------------------------------------------------------- /borax/ui/widgets.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | 4 | class CounterLabel(tk.Label): 5 | def __init__(self, parent, init_value=0, step=1, **kwargs): 6 | self._count_value = tk.IntVar() 7 | self._count_value.set(init_value) 8 | super().__init__(parent, textvariable=self._count_value, **kwargs) 9 | self._step = step 10 | 11 | def increase(self, step=None): 12 | step = step or self._step 13 | self._count_value.set(self._count_value.get() + step) 14 | 15 | def decrease(self, step=None): 16 | step = step or self._step 17 | self._count_value.set(self._count_value.get() - step) 18 | 19 | @property 20 | def count_value(self): 21 | return self._count_value.get() 22 | 23 | @count_value.setter 24 | def count_value(self, value): 25 | self._count_value.set(value) 26 | 27 | 28 | class TimerLabel(CounterLabel): 29 | def __init__(self, parent, interval=1000, **kwargs): 30 | super().__init__(parent, **kwargs) 31 | self._interval = interval 32 | self._state = False 33 | self._timer_id = None 34 | 35 | def _timer(self): 36 | if self._state: 37 | self.increase() 38 | self._timer_id = self.after(self._interval, self._timer) 39 | 40 | def start_timer(self): 41 | if not self._state: 42 | self._state = True 43 | self._timer() 44 | 45 | def stop_timer(self): 46 | self._state = False 47 | if self._timer_id: 48 | self.after_cancel(self._timer_id) 49 | self._timer_id = None 50 | 51 | def reset(self): 52 | self.stop_timer() 53 | self.count_value = 0 54 | 55 | @property 56 | def state(self): 57 | return self._state 58 | 59 | 60 | class MessageLabel(tk.Label): 61 | """A label that can show text in a short time.Variable binding is not supported.""" 62 | _key2colors = {'error': 'red', 'warning': 'orange', 'success': 'green'} 63 | 64 | def show_text(self, text: str, text_color: str = 'black', splash_ms: int = 0): 65 | self.config({'text': text, 'fg': MessageLabel._key2colors.get(text_color, text_color)}) 66 | if splash_ms: 67 | self.after(splash_ms, self._clear) 68 | 69 | def show_error_splash(self, text: str, splash_ms: int = 1000): 70 | self.show_text(text, text_color='error', splash_ms=splash_ms) 71 | 72 | def show_warning_splash(self, text: str, splash_ms: int = 1000): 73 | self.show_text(text, text_color='warning', splash_ms=splash_ms) 74 | 75 | def show_success_splash(self, text: str, splash_ms: int = 1000): 76 | self.show_text(text, text_color='success', splash_ms=splash_ms) 77 | 78 | def _clear(self): 79 | self.config({'text': ''}) 80 | -------------------------------------------------------------------------------- /borax/utils.py: -------------------------------------------------------------------------------- 1 | import collections.abc 2 | from functools import reduce 3 | 4 | 5 | def _resolve_value(val, args=None, kwargs=None): 6 | if callable(val): 7 | args = args or [] 8 | kwargs = kwargs or {} 9 | return val(*args, **kwargs) 10 | else: 11 | return val 12 | 13 | 14 | def force_iterable(obj): 15 | if not isinstance(obj, collections.abc.Iterable) or isinstance(obj, str): 16 | return [obj] 17 | return obj 18 | 19 | 20 | def safe_chain_getattr(obj, attr): 21 | """recourse through an attribute chain to get the ultimate value.""" 22 | return reduce(getattr, attr.split('.'), obj) 23 | 24 | 25 | def chain_getattr(obj, attr, value=None): 26 | """Get chain attribute for an object. 27 | """ 28 | try: 29 | return _resolve_value(safe_chain_getattr(obj, attr)) 30 | except AttributeError: 31 | return value 32 | 33 | 34 | def get_item_cycle(data, index, start=0): 35 | """ 36 | get_item_cycle(data, index) == list(itertools.cycle(data))[:index-start][-1] 37 | :param data: 38 | :param index: 39 | :param start: 40 | :return: 41 | """ 42 | length = len(data) 43 | return data[((index - start) % length + length) % length] 44 | 45 | 46 | def firstof(iterable, func=None, default=None): 47 | for param in iterable: 48 | if callable(func): 49 | r = func(param) 50 | else: 51 | r = param 52 | if r: 53 | return r 54 | return default 55 | 56 | 57 | def trim_iterable(iterable, limit, *, split=None, prefix='', postfix=''): 58 | """trim the list to make total length no more than limit.If split specified,a string is return. 59 | :return: 60 | """ 61 | if split is None: 62 | sl = 0 63 | join = False 64 | else: 65 | sl = len(split) 66 | join = True 67 | result = [] 68 | rl = 0 69 | for element in iterable: 70 | element = prefix + element + postfix 71 | el = len(element) 72 | if len(result) > 0: 73 | el += sl 74 | rl += el 75 | if rl <= limit: 76 | result.append(element) 77 | else: 78 | break 79 | if join: 80 | result = split.join(result) 81 | return result 82 | 83 | 84 | def chunks(iterable, n): 85 | """Yield successive n-sized chunks from iterable object. https://stackoverflow.com/a/312464 """ 86 | for i in range(0, len(iterable), n): 87 | yield iterable[i:i + n] 88 | 89 | 90 | def flatten(iterable): 91 | """flat a iterable. https://stackoverflow.com/a/2158532 92 | """ 93 | for el in iterable: 94 | if isinstance(el, collections.abc.Iterable) and not isinstance(el, (str, bytes)): 95 | yield from flatten(el) 96 | else: 97 | yield el 98 | 99 | 100 | def force_list(val, sep=','): 101 | if isinstance(val, (list, set, tuple)): 102 | return tuple(val) 103 | elif isinstance(val, str): 104 | return tuple(val.split(sep)) 105 | else: 106 | return val, 107 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - borax/ui/*.py 4 | - borax/apps/festival_creator.py 5 | - borax/calendars/datepicker.py 6 | - borax/calendars/ui.py 7 | - borax/capp/*.py 8 | status: 9 | project: 10 | default: 11 | target: 85% 12 | threshold: 5% 13 | patch: 14 | default: 15 | target: 85% 16 | threshold: 5% 17 | changes: no 18 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Borax - python农历&节日工具库 - 中文数字/设计模式/树形结构 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/borax.svg)](https://pypi.org/project/borax) 4 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/borax.svg)](https://pypi.org/project/borax) 5 | ![Python package](https://github.com/kinegratii/borax/workflows/Python%20package/badge.svg) 6 | ![Codecov](https://codecov.io/github/kinegratii/borax/coverage.svg) 7 | ![GitHub license](https://img.shields.io/github/license/kinegratii/borax) 8 | [![borax](https://snyk.io/advisor/python/borax/badge.svg)](https://snyk.io/advisor/python/borax) 9 | 10 | Borax 是一个Python3工具集合库。 11 | 12 | 本文档的所有内容都是基于最新版本,函数和类签名的变化参见各自的文档说明。 13 | 14 | 本项目代码仓库位于 [https://github.com/kinegratii/borax/](https://github.com/kinegratii/borax/) 。同时使用 Gitee 作为国内镜像,位于 [https://gitee.com/kinegratii/borax](https://gitee.com/kinegratii/borax) 。 15 | 16 | ## 话题(Topics) 17 | 18 | - **Borax.Calendar**: [农历](guides/lunardate) | [节日(festivals2)](guides/festivals2) | [节日集合库(FestivalLibrary)](guides/festivals2-library) | [编码序列化](guides/festivals2-serialize) | [生日](guides/birthday) | [节日界面库](guides/festivals2-ui) | [工具类](guides/calendars-utils) 19 | - **Borax.Datasets**: [数据连接(Join)](guides/join) | [列选择器(fetch)](guides/fetch) 20 | - **Borax.DataStructures**: [树形结构](guides/tree) | [cjson](guides/cjson) 21 | - **Borax.Numbers:**: [中文数字](guides/numbers) | [百分数](guides/percentage) 22 | - **Borax.Pattern**: [单例模式](guides/singleton) | [选项Choices](guides/choices) 23 | - **其他**: [序列号生成器(Pool)](guides/serial_pool) | [Tkinter界面](guides/ui) 24 | - **已废弃**: [节日](guides/festival) 25 | 26 | ## 文章(Posts) 27 | 28 | - [农历与节日](guides/festivals2-usage) 29 | 30 | ## 开发(Development) 31 | 32 | - **开发环境**: python3.11.7 33 | - **集成测试环境**: python3.9 - 3.12 34 | - **代码仓库**:[Github](https://github.com/kinegratii/borax/) | [Gitee (镜像)](https://gitee.com/kinegratii/borax) 35 | - **项目开发**: [版本日志](changelog) | [技术文档(外链)](http://fd8cc08f.wiz06.com/wapp/pages/view/share/s/3Zzc2f0LJQ3w2TWIQb0ZMSna1zg4gs1vPQmb2vlh9M2zhqK8) 36 | - **发布日志**: [v3.5](release-note/v350) | [v3.5.6](release-note/v356) | [v4.0.0](release-note/v400) | [v4.1.0](release-note/v410) 37 | 38 | ## 快速开始(Quickstart) 39 | 40 | ### 安装 41 | 42 | Borax 的 python 版本要求如下 43 | 44 | | borax 版本 | python版本 | 维护状态 | 45 | | ------ | ------ | ------ | 46 | | 4.1.x | 3.9+ | 维护开发 | 47 | | 4.0.0 | 3.7+ | 不再维护 | 48 | | 3.x | 3.5+ | 不再维护 | 49 | 50 | 可以通过 *pip* 安装 : 51 | 52 | ```shell 53 | $ pip install borax 54 | ``` 55 | 56 | ### 导入 57 | 58 | 一般来说, 作为功能的代码基本组织形式,建议导入 包(Package) 和 模块(Module) 。 59 | 60 | 例如,导入 `choices`: 61 | 62 | ```python 63 | from borax import choices 64 | 65 | class OffsetChoices(choices.ConstChoices): 66 | up = choices.Item((0, -1), 'Up') 67 | down = choices.Item((0, 1), 'Down') 68 | left = choices.Item((-1, 0), 'Left') 69 | right = choices.Item((0, 1), 'Right') 70 | ``` 71 | 72 | 在某些情况下,也可以直接导入模块的 类(Class) 或 变量(Variate)。 73 | 74 | ```python 75 | from borax.patterns.lazy import LazyObject 76 | 77 | class Point(object): 78 | def __init__(self, x, y): 79 | self.x = x 80 | self.y = y 81 | 82 | p = LazyObject(Point,args=[1,2]) 83 | print(p.x) 84 | ``` 85 | 86 | ### 函数 87 | 88 | borax 库在函数定义和调用方面,尽可能按照 [PEP3102](https://www.python.org/dev/peps/pep-3102/) 声明函数参数,即某些参数必须以关键字形式传入参数。 89 | 90 | ``` 91 | borax.choices.Items(value, display=None, *, order=None) 92 | ``` 93 | 94 | ### 类型标注 95 | 96 | 从 v1.2.0 开始,部分模块支持 [Typing Hint](https://docs.python.org/3/library/typing.html) 。 -------------------------------------------------------------------------------- /docs/capp_changelog.md: -------------------------------------------------------------------------------- 1 | # Borax.Capp 变更日志 2 | 3 | Borax.Capp 是一个自带基于 tkinter 开发的日历应用程序。其版本号同 Borax 库。 4 | 5 | ## v4.1.3 6 | 7 | 修改默认节日库源为 `basic1` 8 | 9 | ## v4.1.1 10 | 11 | - 优化节日创建界面布局 12 | 13 | ## v4.1.0 14 | 15 | - 发布 Borax.Capp -------------------------------------------------------------------------------- /docs/develop_note.md: -------------------------------------------------------------------------------- 1 | # 开发笔记 2 | 3 | ## 代码编写 4 | 5 | ### python版本约束 6 | 7 | Borax 4.1.0开始,要求python最低版本为3.9,主要是引入了新的特性,包括: 8 | 9 | - `functools.cached_property` 装饰器 (python3.8+) 10 | - `typing.Literal` 类型注释(python3.8+) 11 | 12 | ### 代码风格 13 | 14 | 项目代码风格以 [PEP8](https://peps.python.org/pep-0008/) + pycharm 的配置为基准,并增加下列的一些自定义规则。 15 | 16 | - 代码每行长度限制为120 17 | - 函数复杂度限制为25 18 | - 禁止使用 `\` 作为代码行分割的标志,需使用括号 19 | - 不再接受注释方式的类型声明,如 `a = 2 # type:int` 应该为 `a:int = 2` (pyflake触发 `F401` 警告) 20 | 21 | ### API稳定性 22 | 23 | Borax 保证API的稳定性,使用 `warnings` 模块标识已经被废弃的类和函数,并在首次标识之后的2-3个系列版本移除这些类和函数。 24 | 25 | ## 项目构建 26 | 27 | ### 配置文件 28 | 29 | Borax 默认使用 *pyproject.toml* 文件作为项目配置文件,具体包括单元测试、静态检查等内容。 30 | 31 | *pyproject.toml* 配置文件目前包括以下内容: 32 | 33 | | 功能 | 开发库 | 独立配置文件 | pyproject.toml配置段 | 备注 | 34 | | ------------ | -------- | ------------ | -------------------- | -------------------------- | 35 | | 项目基本信息 | - | | [project] | | 36 | | 单元测试 | nose2 | | | | 37 | | 覆盖率 | coverage | | [tool.coverage] | | 38 | | 静态检查 | flake8 | | [tool.flake8] | 通过 Flake8-pyproject 实现 | 39 | | 静态检查 | pylint | .pylintrc | | 配置项过多,不进行迁移 | 40 | | 项目构建 | build | | [tool.setuptool] | | 41 | 42 | 43 | 44 | ### 项目构建 45 | 46 | 项目使用 `build` 作为包构建工具,使用下列命令生成 wheel 文件。 47 | 48 | ```shell 49 | python -m build -w 50 | ``` 51 | 52 | ## 文档 53 | 54 | ### 文档编写 55 | 56 | 除了常规的模块文档外,项目包括以下两种日志文档: 57 | 58 | - 更新日志:每个版本的changelog。 59 | - 发布日志:某些重要版本的 release note,每个版本单独一篇文章。 60 | 61 | ### 文档生成 62 | 63 | Borax项目使用 [Material for MkDocs ](https://squidfunk.github.io/mkdocs-material/) 作为文档生成工具,不再支持 docsify 文档生成工具。 64 | -------------------------------------------------------------------------------- /docs/guides/birthday.md: -------------------------------------------------------------------------------- 1 | # birthday 模块 2 | 3 | > 模块:`borax.calendars.birthday` 4 | 5 | 6 | 7 | > Add in v4.1.3:新增 `BirthdayCalculator` 类。 8 | 9 | `birthday` 模块提供了常用的两种年龄计算方法。 10 | 11 | | 年龄 | 说明 | 12 | | ----------------- | -------------------------------------------------------- | 13 | | 虚岁(Nominal Age) | 在中国传统习俗,出生时即为 1 岁,每逢农历新年增加 1 岁。 | 14 | | 周岁(Actual Age) | 按公历计算,每过一个公历生日就长一岁。 | 15 | 16 | ## 年龄API(对象式) 17 | 18 | > New in v4.1.3 19 | 20 | ### 基本用法 21 | 22 | 从 v4.1.3 开始,本模块新增对象式的API,所涉及到类如下: 23 | 24 | | 类 | 描述 | 25 | | ------------------ | ------------------------------------- | 26 | | BirthdayCalculator | 计算器 | 27 | | BirthdayResult | 计算得到的结果,为 `dateclass` 数据类 | 28 | 29 | `BirthdayCalculator` 接受一个公历日期或农历日期,使用 `calculate` 方法计算相关结果。 30 | 31 | ```python 32 | my_birthday = date(2000, 3, 4) 33 | my_bc = BirthdayCalculator(my_birthday) 34 | print(my_bc.birthday) 35 | result = my_bc.calculate() 36 | print(asdict(result)) 37 | ``` 38 | 39 | 结果 40 | 41 | ```text 42 | 2000-03-04(正月廿九) 43 | { 44 | 'nominal_age': 26, 45 | 'actual_age': 25, 46 | 'animal': '龙', 47 | 'birthday_str': '2000-03-04(二〇〇〇年正月廿九)', 48 | 'next_solar_birthday': , 49 | 'next_lunar_birthday': , 50 | 'living_day_count': 9150 51 | } 52 | ``` 53 | 54 | ### BirthdayCalculator 55 | 56 | **初始化** 57 | 58 | ```python 59 | BirthdayCalculator(birthday: Union[date, LunarDate]) 60 | ``` 61 | 62 | birthday 为某人的生日日期,接受公历和农历两种形式。 63 | 64 | **计算生日相关信息** 65 | 66 | ``` 67 | BirthdayCalculator.calculate(this_day:Union[date, LuarDate]) -> BirthdayResult 68 | ``` 69 | 70 | 以 this_day 为基准,计算周岁、虚岁、下一次生日等相关信息。`BirthdayResult` 相关信息如下: 71 | 72 | ```python 73 | class BirthdayResult: 74 | nominal_age: int = 0 # 虚岁 75 | actual_age: int = 0 # 周岁 76 | animal: str = '' # 生肖 77 | birthday_str: str = '' # 生日描述字符串 78 | next_solar_birthday: WrappedDate = None # 下一次公历生日 79 | next_lunar_birthday: WrappedDate = None # 下一次农历生日 80 | living_day_count: int = 0 # 总天数 81 | ``` 82 | 83 | **计算农历公历生日在同一天的日期** 84 | 85 | ``` 86 | BirthdayCalculator.list_days_in_same_day(start_date=None, end_date=None)->list[WrappedDate] 87 | ``` 88 | 89 | 计算农历公历生日在同一天的日期。 90 | 91 | ## 年龄API(函数式) 92 | 93 | > 这些函数式API已标记为废弃 deprecated,将在 4.2.0 版本移除。 94 | 95 | ### 虚岁 96 | 97 | **nominal_age(birthday:MDate, today:MDate = None) -> int** 98 | 99 | 例子: 100 | 101 | ```python 102 | from borax.calendars.lunardate import LunarDate 103 | from borax.calendars.birthday import nominal_age 104 | 105 | birthday = LunarDate(2017, 6, 16, 1) 106 | print(nominal_age(birthday, LunarDate(2017, 6, 21, 1))) # 1 107 | print(nominal_age(birthday, LunarDate(2017, 12, 29))) # 1 108 | print(nominal_age(birthday, LunarDate(2018, 1, 1))) # 2 109 | ``` 110 | 111 | ### 周岁(按公历) 112 | 113 | **actual_age_solar(birthday:MDate, today:MDate = None) -> int** 114 | 115 | 例子: 116 | 117 | ```python 118 | from datetime import date 119 | from borax.calendars.birthday import actual_age_solar 120 | 121 | print(actual_age_solar(date(2000, 2, 29), date(2003, 2, 28))) # 2 122 | print(actual_age_solar(date(2000, 2, 29), date(2003, 3, 1))) # 3 123 | ``` 124 | 125 | ### 周岁(按农历) 126 | 127 | **actual_age_lunar(birthday:MDate, today:MDate = None) -> int** 128 | 129 | 例子: 130 | 131 | ```python 132 | from datetime import date 133 | from borax.calendars.birthday import actual_age_lunar 134 | 135 | birthday = date(1983, 5, 20) 136 | 137 | print(actual_age_lunar(birthday, today=date(2007, 5, 23))) # 23 138 | actual_age_lunar(birthday, today=date(2007, 5, 24)) # 24 139 | actual_age_lunar(birthday, today=date(2007, 5, 25)) # 24 140 | ``` -------------------------------------------------------------------------------- /docs/guides/bjson.md: -------------------------------------------------------------------------------- 1 | # bjson 模块 2 | 3 | > 模块:`borax.serialize.bjson` 4 | 5 | > This module has been deprecated in v3.4.0 and will be removed in v4.0. 6 | 7 | ## 使用方法 8 | 9 | bjson 模块实现了一个自定义的 JSONEncoder ,支持通过 `__json__` 方法 encode 自定义对象。 10 | 11 | 例子: 12 | 13 | ```python 14 | import json 15 | 16 | from borax.serialize import bjson 17 | 18 | class Point: 19 | def __init__(self, x, y): 20 | self.x = x 21 | self.y = y 22 | 23 | def __json__(self): 24 | return [self.x, self.y] 25 | 26 | obj = {'point': Point(1, 2)} 27 | output = json.dumps(obj, cls=bjson.BJSONEncoder) 28 | print(output) 29 | ``` 30 | 31 | 输出结果: 32 | 33 | ``` 34 | {"point": [1, 2]} 35 | ``` 36 | 37 | bjson 还提供了类似的 `dumps` / `dump` 方法,默认使用 `BJSONEncoder` 。 38 | 39 | 例如: 40 | 41 | ```python 42 | json.dumps(obj, cls=bjson.BJSONEncoder) 43 | ``` 44 | 45 | 可以简化为: 46 | 47 | ```python 48 | bjson.dumps(obj) 49 | ``` 50 | 51 | 52 | 53 | ## API 54 | 55 | - `borax.bjson.dumps(obj, **kwargs)` 56 | - `borax.bjson.dumps(obj, fp, **kwargs)` 57 | 58 | 和 `json` 模块功能相同,使用 `BJSONEncoder` 编码器。 -------------------------------------------------------------------------------- /docs/guides/borax_calendar_app.md: -------------------------------------------------------------------------------- 1 | # Borax日历应用程序 2 | 3 | > New in 4.1.0 4 | 5 | 从 Borax 4.1.0 开始,Borax 提供两个基于 Borax.Calendar 的日历应用。 6 | 7 | | 应用程序 | 功能 | 启动命令 | 8 | | ---- | ---- | ---- | 9 | | 日历应用 | 公农历日期显示,及其他日期工具 | `python -m borax.capp` | 10 | | 节日创建器 | 创建节日库 | `python -m borax.capp creator` | 11 | 12 | ## 日历应用 13 | 14 | ![borax_calendar](../images/app_borax_calendar.png) 15 | 16 | 主要功能: 17 | 18 | - 显示带有基本节日的日历 19 | - 日期计算工具 20 | 21 | ## 节日创建器 22 | 23 | ![festival_creator](../images/app_festival_creator.png) 24 | 25 | 主要功能: 26 | 27 | - 创建节日 28 | - 导出 csv文件 -------------------------------------------------------------------------------- /docs/guides/calendars-utils.md: -------------------------------------------------------------------------------- 1 | # 日期工具库 2 | 3 | Borax.Calendars 提供了一系列适用于常见场景的工具方法。这些方法都定义在 `borax.calendars.SCalendars` (公历相关)和 `borax.calendars.LCalendars` (农历相关)类中。 4 | 5 | 6 | 7 | ## 公历工具SCalendars 8 | 9 | > Add in v3.4.0 10 | 11 | - `SCalendars.get_last_day_of_this_month(year: int, month: int) -> date` 12 | 13 | 返回year年month月的最后一天日期。 14 | 15 | - `SCalendars.get_fist_day_of_year_week(year: int, week: int) -> date` 16 | 17 | 返回year年第week个星期第一天的日期。 18 | 19 | ## 三伏数九天 - ThreeNineUtils 20 | 21 | > Add in v3.5.1 22 | 23 | 三伏天的描述如下: 24 | 25 | 我国传统的推算方法规定,夏至以后的第3个庚日、第4个庚日分别为初伏(头伏)和中伏的开始日期,立秋以后的第一个庚日为末伏的第一天。因为每个庚日之间相隔10天,所以初伏、末伏规定的时间是10天。又因为每年夏至节气后的第3个庚日(初伏)出现的迟早不同,中伏的天数就有长有短,可能是10天,也可能是20天。 26 | 27 | 数九的描述如下: 28 | 29 | 从冬天的冬至算起(冬至即一九第1天),每九天为一"九",第一个九天叫做"一九",第二个九天叫"二九",依此类推,数到"九九"八十一天。 30 | 31 | ### API 32 | 33 | `ThreeNineUtils` 类提供了有关三伏数九的计算函数。 34 | 35 | - **ThreeNineUtils.get_39label(date_obj: Union[date, LunarDate]) -> str** 36 | 37 | 返回某一个日期的相关标签,该标签格式为“初伏/中伏/末伏/一九/二九/.../八九/九九第x天”。如果不是,则返回空字符串。 38 | 39 | ```python 40 | from datetime import date 41 | from borax.calendars.utils import ThreeNineUtils 42 | print(ThreeNineUtils.get_39label(date(2021, 7, 21))) # '中伏第1天' 43 | ``` 44 | 45 | - **ThreeNineUtils.get_39days(year: int) -> Dict[str, date]** 46 | 47 | 返回某一个公历年份的三伏数九天起始日期的全部信息,如 `ThreeNineUtils.get_39days(2021)` 的返回值如下: 48 | 49 | ```pythonconsole 50 | {'一九': datetime.date(2021, 12, 21), 51 | '七九': datetime.date(2022, 2, 13), 52 | '三九': datetime.date(2022, 1, 8), 53 | '中伏': datetime.date(2021, 7, 21), 54 | '九九': datetime.date(2022, 3, 3), 55 | '二九': datetime.date(2021, 12, 30), 56 | '五九': datetime.date(2022, 1, 26), 57 | '八九': datetime.date(2022, 2, 22), 58 | '六九': datetime.date(2022, 2, 4), 59 | '初伏': datetime.date(2021, 7, 11), 60 | '四九': datetime.date(2022, 1, 17), 61 | '末伏': datetime.date(2021, 8, 20)} 62 | 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /docs/guides/cjson.md: -------------------------------------------------------------------------------- 1 | # cjson 模块 2 | 3 | > 模块:`borax.serialize.cjson` 4 | 5 | ## v3.4更新 6 | 7 | 自v3.4.0 开始: 8 | 9 | 1. 原来的 `bjson` 和 `cjson` 将合并为 `cjson`,`cjson` 现在也支持 `__json__` 定义。 10 | 2. 编码函数 `cjson.to_serializable` 将重名为 `cjson.encoder`。 11 | 3. `bjson` 模块 和 `cjson.to_serializable` 函数别名将在v4.0版本移除。 12 | 13 | ## 使用方法 14 | 15 | ### 基础原理 16 | 17 | 在使用 `json.dump/json.dumps` 序列化 Python 对象时,对于无法序列化的类型,用户必须实现自己的序列化逻辑(如 `json.JSONEncoder`),这就是参数 `default` (基于函数)和 `cls` (基于类)的作用。 18 | 19 | `cjson.encoder` 实现了一个基于函数的 `json.JSONEncoder` ,用于 `default` 参数。 20 | 21 | ```python 22 | import json 23 | from borax.serialize import cjson 24 | 25 | json.dumps([1, 2, 3], default=cjson.encoder) 26 | ``` 27 | 28 | `cjson.encoder` 支持两种方式的序列化逻辑定义。 29 | 30 | ### 分开定义 31 | 32 | 即 在 *需要序列化的自定义类* 一侧使用 `__json__` 方法定义。 33 | 34 | 例子: 35 | 36 | ```python 37 | class Point: 38 | def __init__(self, x, y): 39 | self.x = x 40 | self.y = y 41 | 42 | def __json__(self): 43 | return [self.x, self.y] 44 | ``` 45 | 46 | ### 集中定义 47 | 48 | 即 在 *cjson模块* 一侧使用 `cjson.encoder.register` 装饰器定义。 例子: 49 | 50 | ```python 51 | from borax.serialize import cjson 52 | 53 | class EPoint: 54 | def __init__(self, x, y): 55 | self.x = x 56 | self.y = y 57 | 58 | 59 | @cjson.encoder.register(EPoint) 60 | def encode_epoint(o): 61 | return [o.x, o.y] 62 | ``` 63 | 64 | `encoder.register` 是一个标准的装饰器,上述也可以使用下面的简便形式: 65 | 66 | ``` 67 | cjson.encoder.register(EPoint, lambda o: [o.x, o.y]) 68 | ``` 69 | 70 | ### 调用和序列化 71 | 72 | 使用 `cjson.dumps` 即可。 73 | 74 | ```python 75 | from borax.serialize import cjson 76 | 77 | p1 = Point(1, 2) 78 | p2 = EPoint(1, 2) 79 | 80 | output1 = cjson.dumps({'point1':p1, 'point2':p2}) # 输出 {"point1": [1, 2], "point2": [1, 2]} 81 | ``` 82 | 83 | 该函数按照下列方式执行序列化逻辑 : 84 | 85 | 1. 调用 `encoder`,如果抛出 `TypeError` 将忽略这种方式。 86 | 2. `__json__` 87 | 88 | 即,对于具有两种定义方式的同一个类型, *集中定义* 方式将优先于 *分开定义* 方式。 89 | 90 | ```python 91 | from borax.serialize import cjson 92 | 93 | class Pt: 94 | def __init__(self, x, y): 95 | self.x = x 96 | self.y = y 97 | 98 | def __json__(self): 99 | return [self.x, self.y] 100 | 101 | @cjson.encoder.register(Pt) 102 | def encode_pt(p): 103 | return {'x': p.x, 'y': p.y} 104 | 105 | obj = {'point': Pt(1, 2)} 106 | print(cjson.dumps(obj)) # 输出:'{"point": {"x": 1, "y": 2}}' 107 | 108 | cjson.encoder.register(Pt, cjson.encode_object) 109 | 110 | obj = {'point': Pt(1, 2)} 111 | print(cjson.dumps(obj)) # 输出:'{"point": [1, 2]}' 112 | ``` 113 | 114 | ### encoder进阶: dispatch容器 115 | 116 | 对于上层调用者 `json` 来说, `cjson.encoder` 和 `cjson.to_serializable` 是一样的,都是 callable 。 117 | 118 | 但是在内部实现上,`cjson.encoder` 更像是一个 Mapping 容器,由 `{类对象: callable }` 组成。这也是我们使用 *名词* 命名该函数的重要原因。 119 | 120 | - 只能使用 `register` 方法添加新的映射实现,且没有 unregister / delete 等方法(参考 [这里](https://stackoverflow.com/a/25951784)) 121 | - 内部使用 `weakref.WeakKeyDictionary` 组织,可以认为是一个简单的分组。 122 | 123 | ## API 124 | 125 | ### cjson.encode_object 126 | 127 | cjson 默认的编码函数。调用 `__json__` 进行编码。 128 | 129 | ### cjson.encoder 130 | 131 | 使用 [singledispatch](https://docs.python.org/3/library/functools.html#functools.singledispatch) 装饰的json encoder函数,可以直接用于 dump 的 default 参数。 132 | 133 | 使用 `register` 重载新的编码实现, 默认添加了对 `datetime` 、`date` 的序列化支持。 134 | 135 | ```python 136 | encoder.register(datetime, lambda obj: obj.strftime('%Y-%m-%d %H:%M:%S')) 137 | encoder.register(date, lambda obj: obj.strftime('%Y-%m-%d')) 138 | ``` 139 | 140 | ### cjson.CJSONEncoder 141 | 142 | > Add in v3.5.3 143 | 144 | 可用于 `json.dump` 函数cls参数。 145 | 146 | ### cjson.dumps/dump 147 | 148 | cjson 还提供了类似的 `dumps` / `dump` 方法,默认使用 `cjson.encoder` 函数。 149 | 150 | - `borax.serialize.cjson.dumps(obj, **kwargs)` 151 | - `borax.serialize.cjson.dumps(obj, fp, **kwargs)` 152 | 153 | 下面两个语句是等效的。 154 | 155 | ``` 156 | json.dump(obj, default=cjson.encoder) 157 | 158 | json.dump(obj, cls=cjson.CJSONEncoder) 159 | 160 | cjson.dump(obj) 161 | ``` 162 | 163 | ## 参考资料 164 | 165 | - [PEP 443 -- Single-dispatch generic functions](https://www.python.org/dev/peps/pep-0443/) -------------------------------------------------------------------------------- /docs/guides/festival.md: -------------------------------------------------------------------------------- 1 | # festivals 模块 2 | 3 | > 模块: `borax.calendars.festivals` 4 | 5 | > Update in 4.0.0: 本模块已被移除。 6 | > 7 | > 本模块已经标记为 废弃 状态,请使用 `festivals2` 模块。 8 | 9 | ## 日期模式类(DateSchema) 10 | 11 | 在 Borax.Calendars 中,使用 `DateSchema` 表示和定义一个节假日、生日等日期,称之为 **日期模式** 。日期模式是与年份无关的,在给定的年份中,可以计算推导出一个或多个具体的日期。 12 | 13 | 由于节日的表示各不相同,有的是农历日期,有的是公历日期,也有的是按星期确定具体的日期,每个表示形式(模式)使用 `DateSchema` 的一个子类表示。 14 | 15 | `DateSchema` 仅提供了一些简单的接口,无法被实例化。 16 | 17 | - year等于0表示一个模糊日期模式,可匹配任何一年的日期。 18 | - name表示名称、标签,传入任何字符串即可。 19 | 20 | ### SolarSchema 21 | 22 | - **`SolarSchema(month, day, year=YEAR_ANY, reverse=0, **kwargs)`** 23 | 24 | 公历日期,比如元旦(1月1日)、劳动节(5月1日)、国庆节(10月1日)等。 25 | 26 | 当 reverse 等于0时,day 表示具体的“日”;当 reverse 等于1时,day 表示倒数的序数。 27 | 28 | 比如 reverse=1和day=1表示该月的最后一天,即1月31日、3月31日、4月30日...12月31日等。 29 | 30 | 节日编码表示如下: 31 | 32 | | 字段(长度) | schema(1) | year(4) | month(2) | day(2) | reverse(1) | 33 | | ------ | ------ | ------ | ------ | ------ | ------ | 34 | | 描述 | 模式序号 | 年份 | 月份 | 日期或序号 | 是否倒序 | 35 | | 取值范围 | 0 | 0000,1900 - 2101 | 01 - 12 | 01 - 31 | 0,1 | 36 | 37 | ### WeekSchema 38 | 39 | - **`WeekSchema(month, index, week, year=YEAR_ANY, **kwargs)`** 40 | 41 | 依赖于星期的公历日期,比如母亲节(5月的第2个星期日)、父亲节、感恩节等。 42 | 43 | 星期的数值表示参考 `calendar` 模块的定义,即0表示星期一,6表示星期日。 44 | 45 | 比如 `WeekSchema(month=5, index=2, week=6)` 表示母亲节。 46 | 47 | 节日编码表示如下: 48 | 49 | | 字段(长度) | schema(1) | year(4) | month(2) | index(2) | week(1) | 50 | | ------ | ------ | ------ | ------ | ------ | ------ | 51 | | 描述 | 模式序号 | 年份 | 月份 | 序号 | 星期 | 52 | | 取值范围 | 2 | 0000,1900 - 2101 | 01 - 12 | 01 - 05 | 0 - 6 | 53 | 54 | ### TermSchema 55 | 56 | - **`TermSchema(index, year=YEAR_ANY, **kwargs)`** 57 | 58 | 依赖于节气的公历日期,比如清明、冬至。节气按照公历一年先后排序,即0为小寒、1为大寒、6为清明、23为冬至。 59 | 60 | 节日编码表示如下: 61 | 62 | | 字段(长度) | schema(1) | year(4) | - (2) | index(2) | - (1) | 63 | | ------ | ------ | ------ | ------ | ------ | ------ | 64 | | 描述 | 模式序号 | 年份 | - | 节气序号 | - | 65 | | 取值范围 | 4 | 0000,1900 - 2101 | 00 | 01 - 23 | 0 | 66 | 67 | 68 | ### LunarSchema 69 | 70 | - **`LunarSchema(month, day, year=YEAR_ANY, leap=0, ignore_leap=1, **kwargs)`** 71 | 72 | 农历日期,比如七夕(七月初七)、腊八节 (腊月初八)等。 73 | 74 | ignore_leap表示是否忽略闰月进行匹配。比如: 75 | 76 | (1) `LunarSchema(month=6, day=1)` 可以匹配 `LunarDate(2017, 6, 1, 0)` 和 `LunarDate(2017, 6, 1, 1)` 两个日期。 77 | 78 | (2) `LunarSchema(month=6, day=1, ignore_leap=0)` 只匹配 `LunarDate(2017, 6, 1, 0)` 一个日期。 79 | 80 | (3) `LunarSchema(month=6, day=1, leap=1, ignore_leap=1)` 只匹配 `LunarDate(2017, 6, 1, 1)` 一个日期。 81 | 82 | 节日编码表示如下: 83 | 84 | | 字段(长度) | schema(1) | year(4) | month(2) | day(2) | leap(1) | 85 | | ------ | ------ | ------ | ------ | ------ | ------ | 86 | | 描述 | 模式序号 | 年份 | 月份 | 日期 | 是否闰月 | 87 | | 取值范围 | 1 | 0000,1900 - 2100 | 01 - 12 | 01 - 30 | 0,1 | 88 | 89 | ### DayLunarSchema 90 | 91 | - **`DayLunarSchema(month, day, year=YEAR_ANY, reverse=0, **kwargs)`** 92 | 93 | 依赖具体日的农历日期,比如除夕(农历十二月的最后一天)。 94 | 95 | 当 reverse 等于0时,day 表示具体的“日”;当 reverse 等于1时,day 表示倒数的序数。 96 | 97 | 节日编码表示如下: 98 | 99 | | 字段(长度) | schema(1) | year(4) | month(2) | day(2) | reverse(1) | 100 | | ------ | ------ | ------ | ------ | ------ | ------ | 101 | | 描述 | 模式序号 | 年份 | 月份 | 日期或序号 | 是否倒序 | 102 | | 取值范围 | 3 | 0000,1900 - 2100 | 01 - 12 | 01 - 30 | 0,1 | 103 | 104 | ### 方法 105 | 106 | > 以下函数的 `date_obj` 参数可以传入 `datetime.date` 或者 `LunarDate` 对象。 107 | 108 | - **DateSchema.match(data_obj:Union[date, LunarDate]) -> bool** 109 | 110 | 返回 date_obj 的日期和 所代表节日的是否是同一天。 111 | 112 | ``` 113 | >>> from datetime import date 114 | >>>from borax.calendars.festivals import SolarSchema 115 | >>>md = SolarSchema(year=0, month=2, day=14) 116 | >>>md.match(date(2020, 2, 14)) 117 | True 118 | ``` 119 | 120 | - **DateSchema.countdown(data_obj:Union[date, LunarDate]) -> int** 121 | 122 | 返回 data_obj 的日期和本节日下一次日期的距离天数。 123 | 124 | ``` 125 | >>>from borax.calendars.lunar import LunarDate 126 | >>>from borax.calendars.festivals import LunarSchema 127 | >>>ls = LunarSchema(year=0, month=4, day=2) 128 | >>>ls.countdown(LunarDate(2019, 4, 1)) 129 | 1 130 | ``` 131 | 132 | - **DateSchema.resolve(year:int) -> Union[date, LunarDate]** 133 | 134 | 获取某年该日期模式对应的日期对象,具体类型由 `DateSchema.data_class` 确定。 135 | 136 | ``` 137 | >>>from borax.calendars.festivals import LunarSchema 138 | >>>ls = LunarSchema(year=0, month=4, day=2) 139 | >>>ls.resolve(2019) 140 | LunarDate(2019, 4, 2) 141 | ``` 142 | 143 | - **DateSchema.delta(date_obj:Union[date, LunarDate]) -> int** 144 | 145 | 获取和 date_obj 距离的天数,要求 `DateSchema.year` 不为0。 146 | 147 | ``` 148 | >>>from borax.calendars.festivals import LunarSchema 149 | >>>ls = LunarSchema(year=0, month=4, day=2) 150 | >>>ls.delta() 151 | -6550 152 | ``` 153 | 154 | ## 序列化和存储 155 | 156 | `DateSchema` 实现了 `Encoder` 的接口,可以在 `DateSchema` 和字符串之间进行转换, 支持不同形式节日的混合存储。 157 | 158 | 节日编码字符串有长短两种形式。长编码字符串的长度为10,包含了年份字段;短编码字符串长度为6,不包含年份字段。 159 | 160 | ``` 161 | DateSchema ====(encode)====> RawString 162 | 163 | RawString ====(decode)====> DateSchema 164 | ``` 165 | 166 | - **DateSchema.encode(short: bool = True) -> str** 167 | 168 | 转化为编码字符串,这是一个实例方法。 169 | 170 | - classmethod **DateSchema.decode(raw) -> DateSchema** 171 | 172 | 根据编码字符串创建模式对象,这是一个类方法。 173 | 174 | 175 | 例子: 176 | 177 | ```python 178 | from datetime import date 179 | from borax.calendars.festivals import SolarSchema 180 | 181 | ss = SolarSchema.decode('0000012310') 182 | print(ss.match(date(2018, 12, 31))) # True 183 | ``` 184 | 185 | ## 节日计算工具(FestivalFactory) 186 | 187 | `FestivalFactory`类支持从特定的外部文件读取一系列的节日信息。 188 | 189 | ``` 190 | class FestivalFactory(lang=None, file_path=None) 191 | ``` 192 | 193 | `festivals` 也内置了一些国家地区常见的节假日,可以通过 lang 读取这些信息。 194 | 195 | ```python 196 | from datetime import date 197 | from borax.calendars.festivals import FestivalFactory 198 | 199 | factory = FestivalFactory(lang='zh-Hans') 200 | festival = factory.get_festival('元旦') 201 | print(festival.match(date(2018, 1, 1))) # True 202 | ``` 203 | 204 | `FestivalFactory` 提供了一系列函数。 205 | 206 | - `iter_festival_countdown(countdown:Optional[int]=None, today:Union[date, LunarDate], lang: str = 'zh-Hans') -> Iterator[int, List]` 207 | 208 | 计算节日距离某一日期还有多少天,结果按倒计天数分组。 209 | 210 | ```python 211 | list(factory.iter_festival_countdown(30)) # [(7, ['春节']), (16, ['情人节']), (21, ['元宵节'])] 212 | ``` 213 | 214 | - `get_festival(name:str, lang: str = 'zh-Hans') -> DateSchema` 215 | 216 | 获取一个节日的所代表日期对象(DateSchema)。 217 | 218 | ```python 219 | festival = factory.get_festival('春节') 220 | festival.countdown() # 7 221 | ``` 222 | 223 | ## 快捷访问工具 224 | 225 | 为了保持之前的兼容性,`festivals` 提供了几个快捷函数。调用这些函数无需创建相应的 `FestivalFactory` 实例 。 226 | 227 | - `iter_festival_countdown(countdown:Optional[int]=None, today:Union[date, LunarDate]) -> Iterator[int, List]` 228 | - `get_festival(name:str) -> DateSchema` 229 | -------------------------------------------------------------------------------- /docs/guides/festivals2-serialize.md: -------------------------------------------------------------------------------- 1 | # 编码序列化 2 | 3 | > 模块: `borax.calendars.festivals2` 4 | 5 | > Updated in 3.6.0:LunarDate类不再支持直接序列,必须先转化对应的 WrappedDate 对象。 6 | > 7 | > Updated in 3.5.6: 星期型节日(WeekFestival)类支持倒数序号。如:“国际麻风节(1月最后一个星期天)” 8 | > 9 | > Add in 3.5.0 10 | 11 | 12 | 13 | ## 概述 14 | 15 | Borax.Calendars 实现了日期和节日的序列化和持久化,将表示日期或节日的python对象转化为特定格式的字符串存储在外部文件、数据库,也可以通过网络传输对象(如RPC调用)。 16 | 17 | 下面是一些简单的示例: 18 | 19 | | 节日 | 序列化格式 | 节日 | 序列化格式 | 节日 | 序列化格式 | 20 | | ----- | ----- | ----- | ----- | ----- | ----- | 21 | | 元旦 | 001010 | 劳动节 | 005010 | 国庆节 | 010010 | 22 | | 春节 | 101010 | 中秋节 | 108150 | 母亲节 | 205026 | 23 | | 感恩节 | 211043 | 除夕 | 312011 | 清明 | 400060 | 24 | | 2021元旦 | 0202101010 | 2021年劳动节 | 0202105050 | 2021年正月初一 | 1202101010 | 25 | | 2020年闰四月十五 | 1202004151 | - | - | - | - | 26 | 27 | 其中日期使用10位编码表示、节日使用6位编码表。日期字符串编码的第2-5位表示年份信息,其他字段的意义和节日字段相同。 28 | 29 | Borax.Calendars 使用类似下列接口实现序列化和反序列化。 30 | 31 | ```python 32 | class EncodeMixin: 33 | def encode(self) -> str: 34 | pass 35 | @classmethod 36 | def decode(cls, raw:str) -> 'cls': 37 | pass 38 | ``` 39 | 40 | 下表是 `festivals2` 模块中实现该接口的日期节日对象。 41 | 42 | | 日期节日类 | 描述 | 43 | | ------------- | ---------------------------------- | 44 | | WrappedDate | 公历日期、农历日期 | 45 | | Festival | 节日的基类 | 46 | | SolarFestival | 公历节日,如元旦、劳动节、程序员节 | 47 | | LunarFestival | 农历节日,如除夕、中秋节 | 48 | | WeekFestival | 公历星期节日,如母亲节、感恩节 | 49 | | TermFestival | 节气型节日,如清明、冬至 | 50 | | | | 51 | 52 | 和 `festivals` 相比, `festivals2` 不再支持 `LunarDate` 的序列化,必须转化为对应的 `WrappedDate` 对象。这一特定将在Borax3.6版本移除。 53 | 54 | ## 使用方法 55 | 56 | 序列化方法定义在三个模块方法。 57 | 58 | ### encode 59 | 60 | ```python 61 | festivals2.encode(obj: Union[WrappedDate, Festival]) -> str 62 | ``` 63 | 64 | 序列化日期或节日对象,返回特定格式的字符串。 65 | 66 | ### decode 67 | 68 | ```python 69 | festivals2.decode(raw: Union[str, bytes]) -> Union[WrappedDate, Festival] 70 | ``` 71 | 72 | 反序列化日期或节日对象,返回 `WrappedDate` 或 `Festival` 对象。 73 | 74 | ### decode_festival 75 | 76 | ```python 77 | festivals2.decode_festival(raw: Union[str, bytes]) -> Festival 78 | ``` 79 | 80 | 反序列化节日对象,返回 `Festival` 对象。 81 | 82 | ## 基本定义 83 | 84 | ### 编码格式 85 | 86 | 基本的编码格式如下: 87 | 88 | ``` 89 | <1位schema> [<4位year>] <2位month> <2位day> <1位标志> 90 | ``` 91 | 92 | 一些节日在此基础上进行了修改,参见下面的各子Festival的定义。 93 | 94 | ### FestivalSchema 95 | 96 | 第一位表示日期/节日类型,具体的对应关系如下: 97 | 98 | | 首位取值 | 节日类 | 备注 | 99 | | -------- | ------------- | ---------- | 100 | | 0 | SolarFestival / WrappedDate | | 101 | | 1 | LunarFestival / WrapedDate | | 102 | | 2 | WeekFestival | | 103 | | 3 | LunarFestival / WrappedDate | 兼容旧版本DayLunarSchema,只作解析,不作编码 | 104 | | 4 | TermFestival | | 105 | 106 | ### EncoderFlag 107 | 108 | `SolarFestival` 和 `LunarFestival` 的最后一位表示标志位。 109 | 110 | flag 字段使用聚合布尔值表示若干个标志的取值。 111 | 112 | flag 的取值范围为0-F,标志值对应关系如下: 113 | 114 | | flag取值 | day年序号 | 是否每月 | 是否倒序 | 是否闰月 | | 115 | | -------- | --------- | -------- | -------- | -------- | ------------------------ | 116 | | 0 | 0 | 0 | 0 | 0 | 月序号,每年,正序,平月 | 117 | | 1 | 0 | 0 | 0 | 1 | 月序号,每年,正序,闰月 | 118 | | 2 | 0 | 0 | 1 | 0 | 月序号,每年,倒序,平月 | 119 | | 3 | 0 | 0 | 1 | 1 | 月序号,每年,倒序,闰月 | 120 | | 4 | 0 | 1 | 0 | 0 | 月序号,每月,正序,平月 | 121 | | 5 | 0 | 1 | 0 | 1 | 月序号,每月,正序,闰月 | 122 | | 6 | 0 | 1 | 1 | 0 | 月序号,每月,倒序,平月 | 123 | | 7 | 0 | 1 | 1 | 1 | 月序号,每月,倒序,闰月 | 124 | | 8 | 1 | 0 | 0 | 0 | 年序号,每年,正序,平月 | 125 | | 9 | 1 | 0 | 0 | 1 | 年序号,每年,正序,闰月 | 126 | | A | 1 | 0 | 1 | 0 | 年序号,每年,倒序,平月 | 127 | | B | 1 | 0 | 1 | 1 | 年序号,每年,倒序,闰月 | 128 | | C | 1 | 1 | 0 | 0 | 年序号,每月,正序,平月 | 129 | | D | 1 | 1 | 0 | 1 | 年序号,每月,正序,闰月 | 130 | | E | 1 | 1 | 1 | 0 | 年序号,每月,倒序,平月 | 131 | | F | 1 | 1 | 1 | 1 | 年序号,每月,倒序,闰月 | 132 | 133 | ## 日期序列化 134 | 135 | ### WrappedDate 136 | 137 | | 字段(偏移量) | type(1) | year(4) | month(2) | day(2) | flag(1) | 138 | | -------------- | -------- | --------- | -------- | ------ | ------- | 139 | | 描述 | 节日类型 | 年份 | 月份 | 日期 | 标志位 | 140 | | 取值范围 | 0(公历) | 1900-2100 | 01-12 | 01-31 | 0 | 141 | | | 1(农历) | 1900-2100 | - | 01-30 | 0-1 | 142 | 143 | 公历农历日期。 144 | 145 | - 当type=1表示农历日期,此时flag表示是否是农历闰月。 146 | 147 | ## 节日序列化 148 | 149 | ### SolarFestival 150 | 151 | | 字段(偏移量) | type(1) | month(2) | day(2) | flag(1) | 152 | | -------------- | -------- | -------- | --------- | ------- | 153 | | 描述 | 节日类型 | 月份 | 日期 | 标志位 | 154 | | 取值范围 | 0 | 01-12 | 01-31 | 0-7 | 155 | | | 0 | - | 0000-0366 | 8-F | 156 | 157 | 公历型节日。 158 | 159 | - 当 flag 取4-7 时,编码第2-5位共同表示一个day字段。 160 | 161 | ### LunarFestival 162 | 163 | | 字段(偏移量) | type(1) | month(2) | day(2) | flag(1) | 164 | | -------------- | -------- | -------- | --------- | ------- | 165 | | 描述 | 节日类型 | 月份 | 日期 | 标志位 | 166 | | 取值范围 | 1 | 01-12 | 01-31 | 0-7 | 167 | | | 1 | - | 0000-0384 | 8-F | 168 | | | 3 | 00-12 | 01-31 | 0-1 | 169 | 170 | 农历型节日。 171 | 172 | - 当 type = 1 且 flag 取4-7 时,编码第2-5位共同表示一个day字段。 173 | - type = 3时为兼容旧版本之用,此时 0 表示(每年,正序,平月),1表示(每年,倒序,平月) 174 | 175 | ### WeekFestival 176 | 177 | > Updated in 3.5.6: index字段支持10以上数值,表示倒数计数。 178 | 179 | > Updated in 3.5.6: month允许取值0。 180 | 181 | | 字段(偏移量) | type(1) | month(2) | index(2) | week(1) | 182 | | -------------- | -------- | -------- | ----------- | ------- | 183 | | 描述 | 节日类型 | 月份 | 序号 | 星期 | 184 | | 取值范围(每年) | 2 | 01-12 | 01-05,11-15 | 0-6 | 185 | | (每月) | 2 | 00 | 01-05,11-15 | 0-6 | 186 | 187 | 星期型节日。 188 | 189 | - month=0时,表示每月频率。 190 | - index 大于10表示倒数,如12表示倒数第二。 191 | 192 | ### TermFestival 193 | 194 | | 字段(偏移量) | type(1) | nth_t(1) | nth_v(1) | index(2) | day_gz(1) | 195 | | -------------- | -------- | -------- | -------- | -------- | -------------- | 196 | | 描述 | 节日类型 | 序号类型 | - | 节气序号 | 日期天干或地支 | 197 | | 取值范围 | 4 | 0 | 0 | 00-23 | 0 | 198 | | 天干、向前向后 | | 1 | 1-9 | 00-23 | 0-9 | 199 | | 天干、向前计数 | | 2 | 1-9 | 00-23 | 0-9 | 200 | | 地支、向前向后 | | 3 | 1-9 | 00-23 | 0-B | 201 | | 地支、向前计数 | | 4 | 1-9 | 00-23 | 0-B | 202 | 203 | 节气型节日。 204 | 205 | - 节气序号(index )按照公历一年先后排序,即0为小寒、1为大寒、6为清明、23为冬至。 206 | 207 | -------------------------------------------------------------------------------- /docs/guides/festivals2-ui.md: -------------------------------------------------------------------------------- 1 | # 日历界面库 2 | 3 | > Add in 4.0.0 4 | 5 | 本模块提供了若干个可重用的基于 `tkinter.ttk` 的界面组件。 6 | 7 | | 组件 | 类 | 8 | | ------------ | ------------------------------------- | 9 | | 日历组件 | borax.calendars.ui.CalendarFrame | 10 | | 节日表格组件 | borax.calendars.ui.FestivalTableFrame | 11 | | 日期选择框 | borax.calendars.datepicker.ask_date | 12 | 13 | 14 | 15 | ## 日历组件:CalendarFrame 16 | 17 | 18 | 19 | ![calendar-frame](../images/calendar_frame.png) 20 | 21 | ### 创建组件 22 | 23 | CalendarFrame实现了一个简单的公农历组件,该类继承自 `ttk.Frame` 。使用方法如下: 24 | 25 | ```python 26 | from borax.calendars.festivals2 import WrappedDate 27 | from borax.calendars.ui import CalendarFrame 28 | 29 | def on_date_picked(self, wd: WrappedDate): 30 | print(wd) 31 | 32 | cf = CalendarFrame(master, firstweekday=0) 33 | cf.bind_date_selected(on_date_picked) 34 | cf.pack(side='top', expand=True, fill=tk.X) 35 | ``` 36 | 37 | 构造函数的参数及其意义如下: 38 | 39 | | 参数 | 描述 | 说明 | 40 | | -------------------------------------------------- | ------------ | ---------------------------------------------------------- | 41 | | firstweekday:int = 0 | 首列星期 | 0星期一,6星期日 1。 | 42 | | year:int = 0 | 初始公历年份 | year=0或month=0情况下,默认为本月 | 43 | | month:int = 0 | 初始公历月份 | | 44 | | festival_source:Union[str,FestivalLibrary]='empty' | 节日库来源 | 可使用内置源或自定义源。即使不设置节日源,也会显示24节气。 | 45 | | kw | 关键字参数 | 参见 `ttk.Frame` 类 | 46 | 47 | 备注: 48 | 49 | 1. 参数意义同 `calendar.Calendar`的 `firstweekway` 。注意:不能使用 `calendar.MONDAY` 等常量。 50 | 51 | 52 | 53 | 例如:使用自定义节日源 54 | 55 | ```python 56 | from borax.calendars.festivals2 import FestivalLibrary 57 | from borax.calendars.ui import CalendarFrame 58 | 59 | my_library = FestivalLibrary.load_file('./my_festivals.csv') 60 | cf = CalendarFrame(master, festival_source=my_library) 61 | ``` 62 | 63 | 64 | 65 | ### 事件绑定 66 | 67 | `CalendarFrame` 支持使用 `bind_*` 方式设置事件响应函数。 68 | 69 | | 事件名称 | Handler函数签名 | 描述 | 70 | | -------------------- | ----------------------------- | -------------------------- | 71 | | `bind_date_selected` | `handle(wd:WrappedDate)` | 点击日期单元格的响应函数。 | 72 | | `bind_page_changed` | `handle(year:int, month:int)` | | 73 | 74 | ### 布局 75 | 76 | 建议使用 pack 布局。 77 | 78 | ### 属性和方法 79 | 80 | - **year_and_month** 81 | 82 | 返回当前页面的年月。 83 | 84 | - **翻页page_to** 85 | 86 | 将日历翻页到指定月份,trigger控制是否触发 PageChanged 事件。有以下四种使用方式: 87 | 88 | ```python 89 | def CalendarFrame.page_to() # 本月 90 | def CalendarFrame.page_to(month_offset:int) #向前/后几月 91 | def CalendarFrame.page_to(year:int, month:int) # 指定月份 92 | def CalendarFrame.page_to(year:int, month:int, month_offset:int) # 指定月份之前后的月份 93 | ``` 94 | 95 | 示例 96 | 97 | ```python 98 | cf.page_to() # 当前月 99 | cf.page_to(1) # 下一月 100 | cf.page_to(-1) # 上一月 101 | cf.page_to(2022, 9) # 指定月 102 | cf.page_to(2022, 9, 1) # 2022年9月的下一个月 103 | ``` 104 | 105 | 106 | 107 | ## 节日表格组件 FestivalTableFrame 108 | 109 | ![image1](../images/festival_table.png) 110 | 111 | ### 概述 112 | 113 | `FestivalTableFrame` 可显示一个 `FestivalLibrary`对象的所有节日,主要功能: 114 | 115 | - 按表格显示所有节日 116 | - 按类别、倒计天数排序 117 | - 添加新节日 118 | - 删除已有节日 119 | 120 | ### 创建组件 121 | 122 | ```python 123 | FestivalTableFrame(master=None, colunms:Sequeue=None, festival_source:Union[str,FestivalLibrary]='empty', countdown_ordered:bool=False, **kwargs) 124 | ``` 125 | 126 | 构建参数及其意义如下: 127 | 128 | | 参数 | 描述 | 129 | | -------------------------------------------------- | -------------------------------- | 130 | | colunms:Sequeue | 列定义 | 131 | | festival_source:Union[str,FestivalLibrary]='empty' | 节日源,默认为空 | 132 | | countdown_ordered:bool=False | 是否按倒计天数排序。1 | 133 | 134 | 备注: 135 | 136 | 1. v4.1.0新增。 137 | 138 | 表格列定义方式如下: 139 | 140 | ```python 141 | # 只定义列名,宽度默认为200px 142 | colums = ('name','description', 'code') 143 | 144 | # 定义列名和宽度 145 | columns = (("name", 100), ("description", 200), ("code", 120)) 146 | ``` 147 | 148 | 可用的列如下表: 149 | 150 | | 名称 | 描述 | 151 | | ----------- | ------------------ | 152 | | name | 名称 | 153 | | code | 编码 | 154 | | description | 描述字符串 | 155 | | next_day | 下一个日期 | 156 | | ndays | 下一个日期倒计天数 | 157 | 158 | ### 属性 159 | 160 | - **tree_view** 161 | 162 | 关联的树形组件。 163 | 164 | - **festival_library** 165 | 166 | 组件关联的节日库对象。 167 | 168 | - **row_count** 169 | 170 | 表格的条目数。 171 | 172 | ### 方法 173 | 174 | - `add_festival(festival:Festival)` 175 | 176 | 添加新的节日。 177 | 178 | - `add_festivals_from_library(library:FestivalLibrary)` 179 | 180 | 从另一个节日库添加多个节日。 181 | 182 | - `delete_selected_festivals()` 183 | 184 | 删除所选择的节日,支持多选。 185 | 186 | - `notify_data_changed()` 187 | 188 | 重新根据 `FestivalTableFrame.festival_library` 刷新表格数据。 189 | 190 | 例子 191 | 192 | ```python 193 | ftf = FestivalTableFrame(festival_source='basic') 194 | # ... 195 | ftf.festival_library.sorted(key=lambda x:x.code) 196 | ftf.notifiy_data_changed() 197 | ``` 198 | 199 | - `change_festival_source(source:str)` 200 | 201 | v4.1.0新增。更新指定数据源。 202 | 203 | 204 | 205 | ## 日期选择框:ask_date 206 | 207 | ```python 208 | def ask_date() -> Optional[WrappedDate]: 209 | pass 210 | ``` 211 | 212 | 显示日期选择框,并返回选择的日期(类型为 `WrappedDate`),如果未选择,则返回 `None`。 213 | 214 | ```python 215 | import tkinter as tk 216 | from tkinter import ttk 217 | 218 | from borax.calendars.datepicker import ask_date 219 | 220 | 221 | def main(): 222 | root = tk.Tk() 223 | root.title('日期选择器') 224 | root.resizable(False, False) 225 | date_var = tk.StringVar() 226 | entry = ttk.Entry(root, textvariable=date_var) 227 | entry.pack(side='left') 228 | 229 | def askdate(): 230 | wd = ask_date() 231 | print(wd) 232 | if wd: 233 | date_var.set(wd) 234 | 235 | btn = ttk.Button(root, text='点击', command=askdate) 236 | btn.pack(side='left') 237 | root.mainloop() 238 | 239 | 240 | if __name__ == '__main__': 241 | main() 242 | 243 | ``` 244 | 245 | -------------------------------------------------------------------------------- /docs/guides/festivals2-usage.md: -------------------------------------------------------------------------------- 1 | # 农历与节日 2 | 3 | ## 引言 4 | 5 | Borax是一个比较完备的python农历库,覆盖了农历、闰月、干支、节日等内容。相关代码模块如下: 6 | 7 | - borax.calendars.lunardate 农历日期 8 | - borax.calendars.festivals2 节日及其序列化 9 | - borax.calendars.utils 包含工具函数 10 | 11 | ## 农历日期 12 | 13 | ### 创建新日期 14 | 15 | 一个农历日期由年份、月份、日期、闰月标记等组成,依次传入这四个属性就可以创建一个农历日期对象。 16 | 17 | ```python 18 | from borax.calendars.lunardate import LunarDate 19 | 20 | ld = LunarDate(2022,4,1,0) 21 | print(ld) # LunarDate(2022, 4, 1, 0) 22 | ``` 23 | 24 | 正如 `datetime.date` 一样, `LunarDate.strftime` 提供了日期字符串格式化功能。 25 | 26 | ```python 27 | print(ld.strftime('%Y年%L%M月%D')) # '二〇二二年四月初一' 28 | print(ld.strftime('%G')) # '壬寅年甲辰月甲寅日' 29 | ``` 30 | 31 | ### 日期推算 32 | 33 | `LunarDate` 被设计为不可变对象,可以作为字典的key使用,也可以比较大小。 34 | 35 | `LunarDate` 也可以和 `date` 、`timedelta` 进行日期大小计算。 36 | 37 | ```python 38 | ld2 = ld + timedelta(days=10) 39 | print(ld) # LunarDate(2022, 4, 11, 0) 40 | ``` 41 | 42 | ### 传统日期 43 | 44 | `TermUtils` 支持从特定的节气按照干支的顺序计算日期。如初伏、出梅。节气隶属于公历系统,相关函数返回值为 `datetime.date`。 45 | 46 | ```python 47 | dz2022 = TermUtils.day_start_from_term(2022, '冬至') # 计算2022年的冬至是那一天 48 | print(dz2022) # 2022-12-22 49 | 50 | day13 = TermUtils.day_start_from_term(2022, '夏至', 3, '庚') # 初伏:夏至起第3个庚日 51 | print(day13) # 2022-07-16 52 | ``` 53 | 54 | 55 | 56 | ## 农历闰月 57 | 58 | ### 闰月计算 59 | 60 | 为了协调回归年与农历年的矛盾,防止农历年月与回归年即四季脱节,每19年置7闰。Borax提供了一系列有关闰月的计算方法。 61 | 62 | 判断某个月份是否是闰月。 63 | 64 | ```python 65 | from borax.calanders.lunardate import LCalendars 66 | 67 | print(LCalendars.leap_month(2020) == 3) # False;2020年3月不是闰月 68 | print(LCalendars.leap_month(2020) == 4) # True;2020年4月是闰月 69 | print(LCalendars.leap_month(2023) == 2) # True;2023年2月也是闰月 70 | ``` 71 | 72 | 获取有某个闰月的年份。 73 | 74 | ```python 75 | # 在1900~2100两百年中只有2033年是闰十一月 76 | print(LCalendars.get_leap_years(11)) # (2033, ) 77 | 78 | # 在1900~2100两百年没有闰正月的情况 79 | print(LCalendars.get_leap_years(1)) # ( ) 80 | ``` 81 | 82 | 某个农历月份的天数,(29天为小月,30天为大月)。 83 | 84 | ```python 85 | # 农历2022年四月小,五月大 86 | print(LCalendars.ndays(2022, 4)) # 29 87 | print(LCalendars.ndays(2022, 5)) # 30 88 | ``` 89 | 90 | 计算农历年的天数。 91 | 92 | ```python 93 | # 2023年有闰二月。 94 | print(LCalendars.ndays(2022)) # 355 95 | print(LCalendars.ndays(2023)) # 384 96 | ``` 97 | 98 | ### 打印闰月表 99 | 100 | 打印某个时间段内的闰月表: 101 | 102 | ```python 103 | from borax.calendars.lunardate import TextUtils, LCalendars 104 | for year in range(2021, 2050): 105 | leap_month = LCalendars.leap_month(year) 106 | if leap_month == 0: 107 | continue 108 | if LCalendars.ndays(year, leap_month, 1) == 30: 109 | label = '大' 110 | else: 111 | label = '小' 112 | print('{}年闰{}月{}'.format(year, TextUtils.MONTHS_CN[leap_month], label)) 113 | ``` 114 | 115 | 结果输出 116 | 117 | ```text 118 | 2023年闰二月小 119 | 2025年闰六月小 120 | 2028年闰五月小 121 | 2031年闰三月小 122 | 2033年闰冬月小 123 | 2036年闰六月大 124 | 2039年闰五月小 125 | 2042年闰二月小 126 | 2044年闰七月小 127 | 2047年闰五月大 128 | ``` 129 | 130 | ## 节日 131 | 132 | ### 创建节日 133 | 134 | `borax.calendars.festivals2` 模块包含了多种节日类(均继承自 `Festival`),这些类覆盖了大多数类型的公共和传统节日。 135 | 136 | 公历节日 137 | 138 | ```python 139 | new_year = SolarFestival(month=1, day=1) 140 | next_new_year = new_year.at(year=2022) 141 | # 获取2022年的元旦日期 142 | print(repr(next_new_year)) # datetime.date(2022, 1, 1) 143 | # 今天是否是元旦 144 | print(new_year.is_(date.today())) # False 145 | ``` 146 | 147 | 农历除夕 148 | 149 | ```python 150 | new_year_eve = LunarFestival(day=-1) # 每年农历最后一天 151 | next_eve = new_year_eve.at(year=2021) 152 | print(repr(next_eve)) # LunarDate(2021, 12, 29, 0) 153 | ``` 154 | 155 | 国际麻风节 156 | 157 | ```python 158 | import calendar 159 | from borax.calendars.festivals2 import WeekFestival 160 | 161 | leprosy_festival = WeekFestival( 162 | month=1, index=-1, week=calendar.SUNDAY, name='国际麻风节' 163 | ) 164 | print(leprosy_festival.description) # '公历1月最后1个星期日' 165 | for w in leprosy_festival.list_days_in_future(count=5): 166 | print(w.simple_str()) 167 | ``` 168 | 169 | ### 日期列举 170 | 171 | 获取接下去10年的除夕日期 172 | 173 | ```python 174 | new_year_eve = LunarFestival(day=-1) 175 | for ld in new_year_eve.list_days_in_future(count=10): 176 | print(ld) 177 | ``` 178 | 179 | 输出结果: 180 | 181 | ``` 182 | 2022-01-31(二〇二一年腊月廿九) 183 | 2023-01-21(二〇二二年腊月三十) 184 | 2024-02-09(二〇二三年腊月三十) 185 | 2025-01-28(二〇二四年腊月廿九) 186 | 2026-02-16(二〇二五年腊月廿九) 187 | 2027-02-05(二〇二六年腊月廿九) 188 | 2028-01-25(二〇二七年腊月廿九) 189 | 2029-02-12(二〇二八年腊月廿九) 190 | 2030-02-02(二〇二九年腊月三十) 191 | 2031-01-22(二〇三〇年腊月廿九) 192 | ``` 193 | 194 | ## 综合示例 195 | 196 | ### 两头春 197 | 198 | 两头春:一个农历年中,有两个立春。无春年,一个农历年没有立春。 199 | 200 | ```python 201 | from borax.calendars.festivals2 import Period, TermFestival, WrappedDate 202 | licun = TermFestival('立春') 203 | 204 | for lunar_year in range(2000, 2030): 205 | star_date, end_date = Period.lunar_year(lunar_year) 206 | festival_days = licun.list_days(star_date, end_date) 207 | line = '{} {}-{} {} | {}'.format( 208 | lunar_year, 209 | WrappedDate(star_date).simple_str(), 210 | WrappedDate(end_date).simple_str(), 211 | len(festival_days), 212 | ' '.join([str(wd) for wd in festival_days]) 213 | ) 214 | print(line) 215 | ``` 216 | 217 | 结果输出 218 | 219 | ```text 220 | 2000 2000-02-05(正月初一)-2001-01-23(腊月廿九) 0 | 221 | 2001 2001-01-24(正月初一)-2002-02-11(腊月三十) 2 | 2001-02-04(正月十二) 2002-02-04(腊月廿三) 222 | 2002 2002-02-12(正月初一)-2003-01-31(腊月廿九) 0 | 223 | 2003 2003-02-01(正月初一)-2004-01-21(腊月三十) 1 | 2003-02-04(正月初四) 224 | 2004 2004-01-22(正月初一)-2005-02-08(腊月三十) 2 | 2004-02-04(正月十四) 2005-02-04(腊月廿六) 225 | 2005 2005-02-09(正月初一)-2006-01-28(腊月廿九) 0 | 226 | 2006 2006-01-29(正月初一)-2007-02-17(腊月三十) 2 | 2006-02-04(正月初七) 2007-02-04(腊月十七) 227 | 2007 2007-02-18(正月初一)-2008-02-06(腊月三十) 1 | 2008-02-04(腊月廿八) 228 | 2008 2008-02-07(正月初一)-2009-01-25(腊月三十) 0 | 229 | 2009 2009-01-26(正月初一)-2010-02-13(腊月三十) 2 | 2009-02-04(正月初十) 2010-02-04(腊月廿一) 230 | 2010 2010-02-14(正月初一)-2011-02-02(腊月三十) 0 | 231 | 2011 2011-02-03(正月初一)-2012-01-22(腊月廿九) 1 | 2011-02-04(正月初二) 232 | 2012 2012-01-23(正月初一)-2013-02-09(腊月廿九) 2 | 2012-02-04(正月十三) 2013-02-04(腊月廿四) 233 | 2013 2013-02-10(正月初一)-2014-01-30(腊月三十) 0 | 234 | 2014 2014-01-31(正月初一)-2015-02-18(腊月三十) 2 | 2014-02-04(正月初五) 2015-02-04(腊月十六) 235 | 2015 2015-02-19(正月初一)-2016-02-07(腊月廿九) 1 | 2016-02-04(腊月廿六) 236 | 2016 2016-02-08(正月初一)-2017-01-27(腊月三十) 0 | 237 | 2017 2017-01-28(正月初一)-2018-02-15(腊月三十) 2 | 2017-02-03(正月初七) 2018-02-04(腊月十九) 238 | 2018 2018-02-16(正月初一)-2019-02-04(腊月三十) 1 | 2019-02-04(腊月三十) 239 | 2019 2019-02-05(正月初一)-2020-01-24(腊月三十) 0 | 240 | 2020 2020-01-25(正月初一)-2021-02-11(腊月三十) 2 | 2020-02-04(正月十一) 2021-02-03(腊月廿二) 241 | 2021 2021-02-12(正月初一)-2022-01-31(腊月廿九) 0 | 242 | 2022 2022-02-01(正月初一)-2023-01-21(腊月三十) 1 | 2022-02-04(正月初四) 243 | 2023 2023-01-22(正月初一)-2024-02-09(腊月三十) 2 | 2023-02-04(正月十四) 2024-02-04(腊月廿五) 244 | 2024 2024-02-10(正月初一)-2025-01-28(腊月廿九) 0 | 245 | 2025 2025-01-29(正月初一)-2026-02-16(腊月廿九) 2 | 2025-02-03(正月初六) 2026-02-04(腊月十七) 246 | 2026 2026-02-17(正月初一)-2027-02-05(腊月廿九) 1 | 2027-02-04(腊月廿八) 247 | 2027 2027-02-06(正月初一)-2028-01-25(腊月廿九) 0 | 248 | 2028 2028-01-26(正月初一)-2029-02-12(腊月廿九) 2 | 2028-02-04(正月初十) 2029-02-03(腊月二十) 249 | 2029 2029-02-13(正月初一)-2030-02-02(腊月三十) 0 | 250 | ``` 251 | 252 | -------------------------------------------------------------------------------- /docs/guides/fetch.md: -------------------------------------------------------------------------------- 1 | # Fetch 模块 2 | 3 | > 模块:`borax.datasets.fetch` 4 | 5 | ## 函数接口 6 | 7 | `borax.datasets.fetch` 模块实现了从数据列表按照指定的一个或多个属性/键选取数据。 8 | 9 | `fetch` 模块包含了以下几个函数: 10 | 11 | - `fetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None)` 12 | - `ifetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None)` 13 | - `fetch_single(iterable, key, default=EMPTY, getter=None)` 14 | - `ifetch_multiple(iterable, *keys, defaults=None, getter=None)` 15 | - `ifetch_single(iterable, key, default=EMPTY, getter=None)` 16 | 17 | 各个参数意义如下: 18 | 19 | - iterable:数据列表 20 | - key / keys:键值、属性访问方式的索引 21 | - default:默认值,用于选择单个属性 22 | - defaults:默认值字典,用于选择多个属性 23 | - getter:自定义访问回调函数 24 | 25 | 通常使用 `fetch` 函数即可。 26 | 27 | ## 基本使用 28 | 29 | ### 选取单个属性 30 | 31 | 从 `objects` 数据获取 `name` 的数据。 32 | 33 | ```python 34 | from borax.datasets.fetch import fetch 35 | 36 | objects = [ 37 | {'id': 282, 'name': 'Alice', 'age': 30}, 38 | {'id': 217, 'name': 'Bob', 'age': 56}, 39 | {'id': 328, 'name': 'Charlie', 'age': 56}, 40 | ] 41 | 42 | names = fetch(objects, 'name') 43 | print(names) 44 | ``` 45 | 46 | 输出 47 | 48 | ``` 49 | ['Alice', 'Bob', 'Charlie'] 50 | ``` 51 | 52 | ### 选取多个属性 53 | 54 | 从 `objects` 数据获取 `name` 和 `age` 的数据。 55 | 56 | ```python 57 | from borax.datasets.fetch import fetch 58 | 59 | objects = [ 60 | {'id': 282, 'name': 'Alice', 'age': 30}, 61 | {'id': 217, 'name': 'Bob', 'age': 56}, 62 | {'id': 328, 'name': 'Charlie', 'age': 56}, 63 | ] 64 | 65 | names, ages = fetch(objects, 'name', 'age') 66 | print(names) 67 | print(ages) 68 | ``` 69 | 70 | 输出 71 | 72 | ``` 73 | ['Alice', 'Bob', 'Charlie'] 74 | [30, 56, 56] 75 | ``` 76 | 77 | ### 列表型数据 78 | 79 | `fetch` 函数第一个参数 `iterable` 也可以是列表型数据。 80 | 81 | ```python 82 | from borax.datasets.fetch import fetch 83 | 84 | data = [ 85 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 86 | [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 87 | [20, 21, 22, 23, 24, 25, 26, 27, 28, 29] 88 | ] 89 | zeros, twos = fetch(data, 0, 2) 90 | print(zeros) 91 | print(twos) 92 | ``` 93 | 94 | 输出 95 | 96 | ``` 97 | [0, 10, 20] 98 | [2, 12, 22] 99 | ``` 100 | 101 | 此时情况下和 `zip` 函数有相类似的功能,上述例子可改写如下: 102 | 103 | ```python 104 | data = [ 105 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 106 | [10, 11, 12, 13, 14, 15, 16, 17, 18, 19], 107 | [20, 21, 22, 23, 24, 25, 26, 27, 28, 29] 108 | ] 109 | zeros, _, twos, *_ = zip(*data) 110 | print(zeros) 111 | print(twos) 112 | ``` 113 | 114 | 115 | 116 | ## 提供默认值 117 | 118 | 当 `iterable` 数据列表缺少某个属性/键,可以通过指定 `default` 或 `defaults` 参数提供默认值。 119 | 120 | ```python 121 | from borax.datasets.fetch import fetch 122 | 123 | objects = [ 124 | {'id': 282, 'name': 'Alice', 'age': 30, 'gender': 'female'}, 125 | {'id': 217, 'name': 'Bob', 'age': 56}, 126 | {'id': 328, 'name': 'Charlie', 'gender': 'male'}, 127 | ] 128 | 129 | print('Demo for one default value') 130 | genders = fetch(objects, 'gender', default='unknown') 131 | print(genders) 132 | 133 | print('Demo for multiple default values') 134 | ages, genders = fetch(objects, 'age', 'gender', defaults={'age': 0, 'gender':'unknown'}) 135 | print(genders) 136 | print(ages) 137 | ``` 138 | 139 | 结果输出 140 | 141 | ``` 142 | Demo for one default value 143 | ['female', 'unknown', 'male'] 144 | Demo for multiple default values 145 | ['female', 'unknown', 'male'] 146 | [30, 56, 0] 147 | ``` 148 | 149 | ## 属性访问 150 | 151 | 152 | 除了上述的键值访问方式,`fetch` 函数还内置属性访问的获取方式。 153 | 154 | ```python 155 | from borax.datasets.fetch import fetch 156 | 157 | class Point: 158 | def __init__(self, x, y, z): 159 | self.x = x 160 | self.y = y 161 | self.z = z 162 | 163 | 164 | points = [ 165 | Point(1, 2, 3), 166 | Point(4, 5, 6), 167 | Point(7, 8, 9) 168 | ] 169 | 170 | print('Fetch x values:') 171 | x = fetch(points, 'x') 172 | print(x) 173 | 174 | print('Fetch x,y,z values:') 175 | x, y, z = fetch(points, 'x', 'y', 'z') 176 | print(x) 177 | print(y) 178 | print(z) 179 | ``` 180 | 181 | 结果输出 182 | 183 | ``` 184 | Fetch x values: 185 | [1, 4, 7] 186 | Fetch x,y,z values: 187 | [1, 4, 7] 188 | [2, 5, 8] 189 | [3, 6, 9] 190 | ``` 191 | 192 | ## 自定义Getter 193 | 194 | 除了内置的属性访问方式 [itemgetter](https://docs.python.org/3.6/library/operator.html#operator.itemgetter) 和键值访问方式 [attrgetter](https://docs.python.org/3.6/library/operator.html#operator.attrgetter) 外,`fetch` 函数还通过 `getter` 参数支持自定义访问方式。 195 | 196 | getter 需满足下列的几个条件: 197 | 198 | - 是一个函数,命名函数或匿名函数均可 199 | - 该函数必须含有 *item* 和 *key* 两个参数 200 | - 返回是具体的数值 201 | 202 | 例子: 203 | 204 | ```python 205 | from borax.datasets.fetch import fetch 206 | 207 | 208 | class Point: 209 | def __init__(self, index, x, y, z): 210 | self.index = index 211 | self._data = {'x': x, 'y': y, 'z': z} 212 | 213 | def get(self, key): 214 | return self._data.get(key) 215 | 216 | 217 | points = [ 218 | Point('a', 1, 2, 3), 219 | Point('b', 4, 5, 6), 220 | Point('c', 7, 8, 9) 221 | ] 222 | 223 | 224 | def point_getter(item, key): 225 | return item.get(key) 226 | 227 | 228 | print('Fetch x values:') 229 | x = fetch(points, 'x', getter=point_getter) 230 | print(x) 231 | 232 | print('Fetch x,y,z values:') 233 | x, y, z = fetch(points, 'x', 'y', 'z', getter=point_getter) 234 | print(x) 235 | print(y) 236 | print(z) 237 | ``` 238 | 239 | 240 | 结果输出 241 | 242 | ``` 243 | Fetch x values: 244 | [1, 4, 7] 245 | Fetch x,y,z values: 246 | [1, 4, 7] 247 | [2, 5, 8] 248 | [3, 6, 9] 249 | ``` 250 | 251 | 需要注意的是,自定义 Getter 是应用至所有属性的,内置的 *属性访问方式* 和 *键值访问方式* 将不再使用,混用将可能无法获得期望的结果。 252 | 253 | 错误的示例1 254 | 255 | ```bash 256 | >>> indexes, xs = fetch(points, 'index', 'x', getter=point_getter) 257 | [None, None, None] 258 | [1, 4, 7] 259 | ``` 260 | 261 | 错误的示例2 262 | 263 | ```bash 264 | >>> indexes, xs = fetch(points, 'index', 'x') 265 | Traceback (most recent call last): 266 | TypeError: 'Point' object is not subscriptable 267 | ``` 268 | 269 | 应当分别调用 `fetch` 函数。 270 | 271 | 正确的用法 272 | 273 | ```python 274 | x, y = fetch(points, 'x', 'y', getter=point_getter) 275 | 276 | print(x) 277 | print(y) 278 | 279 | indexes = fetch(points, 'index') 280 | print(indexes) 281 | ``` 282 | 283 | 结果输出 284 | 285 | ``` 286 | [1, 4, 7] 287 | [2, 5, 8] 288 | ['a', 'b', 'c'] 289 | ``` -------------------------------------------------------------------------------- /docs/guides/numbers.md: -------------------------------------------------------------------------------- 1 | # numbers 模块 2 | 3 | > 模块:`borax.numbers` 4 | 5 | ## 常量定义 6 | 7 | `numbers` 提供了下列的模块级常量。 8 | 9 | - **numbers.MAX_VALUE_LIMIT = 1_0000_0000_0000** 10 | 11 | 本模块可以处理的数字上限,值为一万亿(10^12) , 超过该值将抛出 `ValueError` 异常,适用本模块的所有函数。 12 | 13 | - **LOWER_DIGITS = '零一二三四五六七八九'** 14 | 15 | 1-9小写数字。 16 | 17 | - **UPPER_DIGITS = '零壹贰叁肆伍陆柒捌玖'** 18 | 19 | 1-9大写数字。 20 | 21 | ## 中文数字 22 | 23 | > Add in v3.4.0 24 | 25 | ### 数字用法规定 26 | 27 | 中文数字按照 **大写/小写** 、**计量/编号** 的方式划分为四种形式。 28 | 29 | | 形式 | 示例(以204为例) | 使用场景 | 30 | | ---------- | ----------------- | ---------------------- | 31 | | 小写、计量 | 二百零四 | 汉字数字形式、法条序号 | 32 | | 大写、计量 | 贰佰零肆 | 财务相关 | 33 | | 小写、编号 | 二百〇四 | 年份 | 34 | | 大写、编号 | 贰佰〇肆 | | 35 | 36 | 37 | 38 | 根据 [《出版物上数字用法(GB/T-15835-2011)》](http://www.moe.gov.cn/ewebeditor/uploadfile/2015/01/13/20150113091154536.pdf) 的规定,汉字 “零” 和 “〇” 是有严格的使用场景。 39 | 40 | ``` 41 | 阿拉伯数字“0”有“零”和“〇”两种汉字书写形式。一个数字用作计量时,其中“0”的汉字书写形式为“零”,用作编号时,“0”的汉字书写形式为“〇”。 42 | 43 |  示例:“3052(个)”的汉字数字形式为“三千零五十二”(不写为“三千〇五十二”) 44 | 45 | “95.06”的汉字数字形式为“九十五点零六”(不写为“九十五点〇六”) 46 | 47 | “公元2012(年)”的汉字数字形式为“二〇一二”(不写为“二零一二”) 48 | 49 | ---- 出版物上数字用法(GB/T-15835-2011) 50 | ``` 51 | 52 | ### API 53 | 54 | 基于上述使用规定, `ChineseNumbers` 类将整数转化为对应的中文数字。 55 | 56 | - **ChineseNumbers.measure_number(num: Union[int, str], upper: bool = False) -> str** 57 | 58 | > Update in v3.5.1: 新增upper参数。 59 | 60 | 将数字转化为 *计量大/小写* 的中文数字,数字0的中文形式为“零”。 61 | 62 | - **ChineseNumbers.order_number(num: Union[int, str], upper: bool = False) -> str** 63 | 64 | 将数字转化为 *编号大/小写* 的中文数字,数字0的中文形式为“〇”。 65 | 66 | > Update in v3.5.1: 新增upper参数。 67 | 68 | - **ChineseNumbers.order_number(num: Union[int, str], upper: bool = False) -> str** 69 | 70 | 将数字转化为 *计量/编号 + 大/小写* 的中文数字,数字0的中文形式为“〇”。 本函数不再推荐直接使用。 71 | 72 | 总结 73 | 74 | | 函数 | 结果 | 备注 | 75 | | -------------------------------- | ----------- | ------------ | 76 | | ChineseNumbers.to_chinese_number(204) | 二百零四 | 小写、计量 | 77 | | ChineseNumbers.to_chinese_number(204, , upper=True) | 贰佰零肆 | 大写、计量 | 78 | | ChineseNumbers.to_chinese_number(204, , upper=False) | 二百〇四 | 小写、编号 | 79 | | ChineseNumbers.to_chinese_number(204, , upper=True, order=True) | 贰佰〇肆 | 大写、编号 | 80 | | ChineseNumbers.measure_number(204) | 二百零四 | 计量数字 | 81 | | ChineseNumbers.order_number(204) | 二百〇四 | 编号数字 | 82 | 83 | 84 | 例子 85 | 86 | ```python 87 | from borax.numbers import ChineseNumbers 88 | 89 | print(ChineseNumbers.to_chinese_number(204)) # 二百零四 90 | print(ChineseNumbers.measure_number(204)) # 二百零四 91 | print(ChineseNumbers.order_number(1056)) # 一千〇五十六 92 | 93 | ``` 94 | 95 | ## 财务大写金额 96 | 97 | > Add in v3.3.0 98 | 99 | finance 提供了一系列的财务金融工具。 100 | 101 | ### 规范依据 102 | 103 | 本函数的书写规则的根据是《会计基础工作规范》。 104 | 105 | ``` 106 | 第五十二条 填制会计凭证,字迹必须清晰、工整,并符合下列要求: 107 | 108 |   (一)阿拉伯数字应当一个一个地写,不得连笔写。阿拉伯金额数字前面应当书写货币币种符号或者货币名称简写和币种符号。币种符号与阿拉伯金额数字之间不得留有空白。凡阿拉伯数字前写有币种符号的,数字后面不再写货币单位。 109 | 110 |   (二)所有以元为单位(其他货币种类为货币基本单位,下同)的阿拉伯数字,除表示单价等情况外,一律填写到角分;无角分的,角位和分位可写“00”,或者符号“——”;有角无分的,分位应当写“0”,不得用符号“——”代替。 111 | 112 |   (三)汉字大写数字金额如零、壹、贰、叁、肆、伍、陆、柒、捌、玖、拾、佰、仟、万、亿等,一律用正楷或者行书体书写,不得用0、一、二、三、四、五、六、七、八、九、十等简化字代替,不得任意自造简化字。大写金额数字到元或者角为止的,在“元”或者“角”字之后应当写“整”字或者“正”字;大写金额数字有分的,分字后面不写“整”或者“正”字。 113 | 114 |   (四)大写金额数字前未印有货币名称的,应当加填货币名称,货币名称与金额数字之间不得留有空白。 115 | 116 |   (五)阿拉伯金额数字中间有“0”时,汉字大写金额要写“零”字;阿拉伯数字金额中间连续有几个“0”时,汉字大写金额中可以只写一个“零”字;阿拉伯金额数字元位是“0”,或者数字中间连续有几个“0”、元位也是“0”但角位不是“0”时,汉字大写金额可以只写一个“零”字,也可以不写“零”字。 117 | ``` 118 | 119 | 对第(五)款最后一项的解释,如540.4 既可以表示 *伍佰肆拾元肆角零分* ,也可以表示 *伍佰肆拾元零肆角零分* ,本函数采用前者表示方法。 120 | 121 | ### 使用示例 122 | 123 | 将数字转化为财务大写金额的字符串,函数签名: 124 | 125 | ```python 126 | to_capital_str(num: Union[int, float, Decimal, str]) -> str 127 | ``` 128 | 129 | 输入值可以为以下几种类型: 130 | 131 | - 数字:如 `32`, `4.56` 等; 132 | - 字符串,如 `'32'`, `'4.56'` 等; 133 | - 小数,如 `decimal.Decimal('8.29')` 等。 134 | 135 | 例子: 136 | 137 | ``` 138 | >>> from borax.numbers import FinanceNumbers 139 | >>> FinanceNumbers.to_capital_str(100000000) 140 | '壹亿元整' 141 | >>>FinanceNumbers.to_capital_str(4578442.23) 142 | '肆佰伍拾柒万捌仟肆佰肆拾贰元贰角叁分' 143 | >>>FinanceNumbers.to_capital_str(107000.53) 144 | 壹拾万柒仟元伍角叁分 145 | ``` 146 | 147 | -------------------------------------------------------------------------------- /docs/guides/percentage.md: -------------------------------------------------------------------------------- 1 | # percentage 模块 2 | 3 | > 模块: `borax.structures.percentage` 4 | 5 | 6 | 7 | ## 模块方法 8 | 9 | ### format_percentage 10 | 11 | > Add in v3.2.0 12 | 13 | ``` 14 | format_percentage(numerator: int, denominator: int, *, places: int = 2, null_val: str = '-') -> str 15 | ``` 16 | 17 | 返回百分数。 18 | 19 | ## Percentage类 20 | 21 | ### 定义 22 | 23 | 该模块仅定义一个 `Percentage` 类,表示具体的百分比数据。类 `__init__` 函数定义如下: 24 | 25 | ```python 26 | def __init__(self, *, total=100, completed=0, places=2, display_fmt='{completed} / {total}', null_val:str='-'): 27 | pass 28 | ``` 29 | 30 | > Changed in v3.2.0: 新增 null_val 参数。 31 | 32 | 各参数意义如下: 33 | 34 | | 参数 | 数据类型 | 意义 | 35 | | ------ | ------ | ------ | 36 | | total | int | 总数 | 37 | | completed | int | 完成数目 | 38 | | places | int | 百分比的小数点,如 place=2,时显示为 34.56% | 39 | | display_fmt | string | 显示格式字符串,可用变量:total, completed | 40 | | null_val | string | 空值的字符串形式 | 41 | 42 | ### 数据属性 43 | 44 | `Percentage` 包含了一系列的数据属性。 45 | 46 | ```python 47 | from borax.structures.percentage import Percentage 48 | 49 | p = Percentage(total=100, completed=34) 50 | 51 | ``` 52 | 53 | 包含以下数据属性。 54 | 55 | | 属性 | 数据类型 | 描述 | 示例 | 56 | | ------ | ------ | ------ | ------ | 57 | | total | int | 总数 | `100` | 58 | | completed | int | 完成数目 | `34` | 59 | | percent | float | 百分比数值 | `0.34` | 60 | | percent_display | string | 百分比字符串 | `'34.00%'` | 61 | | fraction_display | string | 分数字符串 | `'34 / 100'`| 62 | | display | string | 分数字符串 | `'34 / 100'`| 63 | 64 | 备注: 65 | 66 | - `fraction_display` 为 v3.2.0 新增。 67 | 68 | ### 方法 69 | 70 | - **`increase(value=1)`** 71 | 72 | 增加进度值,默认间隔为1 73 | 74 | - **`decrease(value=1)`** 75 | 76 | 减少进度值,默认为1 77 | 78 | - **`as_dict(prefix='')`** 79 | 80 | 导出字典格式,`prefix` 为键(key)的前缀。 81 | 82 | -------------------------------------------------------------------------------- /docs/guides/serial_generator.md: -------------------------------------------------------------------------------- 1 | # 序列号分配生成工具 2 | 3 | > 模块:`borax.counters.serials` 4 | 5 | 6 | 7 | ## 背景 8 | 9 | 10 | 从整数范围分别生成若干个可用序列号。支持: 11 | 12 | - 数字或字符串格式 13 | - 多线程 14 | 15 | [场景示例] 在业务系统中,设备序列号是唯一的,由“GZ” + 4位数字组成,也就是可用范围为 GZ0000 - GZ9999。当每次添加设备使,函数 `generate_serials` 能够生成未被使用的序列号。 16 | 17 | 比如某一时刻数据库设备数据如下: 18 | 19 | | ID | 序列号 | 20 | | ------ | ------ | 21 | | 1 | GZ0000 | 22 | | 2 | GZ0001 | 23 | | 45 | GZ0044 | 24 | | 46 | GZ0045 | 25 | 26 | 接下去可用的三个序列号依次为 GZ0046、GZ0047、GZ0048 。 27 | 28 | 使用代码如下: 29 | 30 | ```python 31 | from borax.counters.serials import StringSerialGenerator 32 | 33 | ssg = StringSerialGenerator('GZ', digits=4) 34 | ssg.add(['GZ0000', 'GZ0001', 'GZ0044', 'GZ0045']) 35 | ssg.generate(3) 36 | ``` 37 | 38 | 39 | ## API 40 | 41 | ### generate_serials 42 | 43 | 函数签名 44 | 45 | ```python 46 | def generate_serials(upper: int, num: int = 1, lower: int = 0, serials: Iterable[int] = None) -> List[int]: 47 | pass 48 | ``` 49 | 50 | 参数: 51 | 52 | - upper:整数范围的上限,不包含此数。 53 | - lower:整数范围的下限,包含此数。 54 | - num:分配ID的数量。 55 | - serials:已经存在的ID,集合、列表类型。 56 | 57 | 如果无法生成,将抛出 `ValueError` 异常。 58 | 59 | ### SerialGenerator 60 | 61 | ID分配生成器,可以在多次生成操作之间保存和维持ID数据。 62 | 63 | 例如:下面的例子从0到9生成3个序列号。 64 | 65 | ```python 66 | sg = SerialGenerator(upper=10) 67 | sg.add([0, 1, 2]) 68 | res = sg.generate(3) # [3, 4, 5] 69 | ``` 70 | 71 | **generate** 72 | 73 | `generate(self, num: int) -> List[int]` 74 | 75 | 返回若干个可用的序列号,内部调用 `generate_serials` 函数。 76 | 77 | **add** 78 | 79 | `add(self, elements: Iterable[int]) -> None` 80 | 81 | 添加若干个序列号。 82 | 83 | 84 | **remove** 85 | 86 | `remove(self, elements: Iterable[int]) -> None` 87 | 88 | 删除若干个序列号。 89 | 90 | 91 | 92 | ### StringSerialGenerator 93 | 94 | 基于字符串形式的序列号生成器。 -------------------------------------------------------------------------------- /docs/guides/serial_pool.md: -------------------------------------------------------------------------------- 1 | # 序列号分配生成工具(Pool) 2 | 3 | > 模块:`borax.counters.serial_pool` 4 | 5 | > Add in v3.4.0 6 | 7 | 模块 `borax.counters.serial_pool` 。 8 | 9 | ## 概述 10 | 11 | `serial_pool` 模块涉及到以下几个概念: 12 | 13 | - `SerialPool` 生成器 14 | - `SerialElement` 具体的序列号实体类,含有 value 和 label 两个属性 15 | - `SerialStore` 存储序列号池的实现,包括内存、数据库。 16 | 17 | ## 使用示例 18 | 19 | ### 定义范围 20 | 21 | `SerialNoPool` 支持以下方式的定义: 22 | 23 | 24 | 25 | 以下是几个常用的示例: 26 | 27 | | 定义 | value 范围 | label 范围 | 28 | | ----------------------------------------------------------- | ---------- | ----------------------- | 29 | | SerialNoPool(lower=0, upper=100) | 0 ~ 99 | - | 30 | | SerialNoPool(base=10, digits=2) | 0 ~ 99 | - | 31 | | SerialNoPool(label_fmt='LC{no:06d}') | 0 ~ 999999 | 'LC000000' ~ 'LC999999' | 32 | | SerialNoPool(label_fmt='LC{no:04d}', lower=101, upper=1000) | 101 ~ 999 | 'LC101' ~ 'LC999' | 33 | | SerialNoPool(label_fmt='FF{no}', base=16, digits=2) | 0 ~ 255 | 'FF00' ~ 'FFFF' | 34 | | | | | 35 | | | | | 36 | 37 | ## 生成序列号 38 | 39 | 手动管理序列号池 40 | 41 | ```python 42 | from borax.counters.serial_pool import SerialNoPool 43 | 44 | pool = SerialNoPool(label_fmt='FF{no}', base=16, digits=2) 45 | data = pool.generate(num=2) 46 | print(data) # ['FF00', 'FF01'] 47 | 48 | pool.add_elements(data) 49 | data = pool.generate(num=2) 50 | print(data) # ['FF02', 'FF03'] 51 | ``` 52 | 53 | 设置序列化池回调 54 | 55 | ```python 56 | from django.db import models 57 | from borax.counters.serial_pool import SerialNoPool 58 | 59 | class Workflow(models.Model): 60 | code = models.CharField(unquie=True, max_length=20) 61 | name = models.CharField(max_length=50) 62 | start_time = models.DatetimeField() 63 | end_time = models.DatetimeField() 64 | 65 | def get_used_workflow_codes(): 66 | return list(Workflow.objects.all().values_list('code', flat=True)) 67 | 68 | pool = SerialNoPool(label_fmt='LC{no:08d}') 69 | pool.set_source(get_used_workflow_codes) 70 | data = pool.generate_labels(num=1) 71 | 72 | pool.add_elements(data) 73 | 74 | ``` 75 | 76 | 77 | ## API 78 | 79 | ### 序列号管理 80 | 81 | - `SerialNoPool.add_elements(elements: ElementsType) -> 'SerialNoPool'` 82 | - `SerialNoPool.remove_elements(elements: ElementsType) -> 'SerialNoPool'` 83 | - `SerialNoPool.set_elements(elements: ElementsType) -> 'SerialNoPool'` 84 | 85 | 实现序列号池的增加、删除、重置。 86 | 87 | - `SerialNoPool.set_source(source: Callable[[], ElementsType]) -> 'SerialNoPool'` 88 | 89 | 设置序列号池的回调函数,在此种方式下,SerialNoPool 内部不再保存具体的序列号集合,而是在每次调用 `generate_*` 方法时重新调用计算。 90 | 91 | ### 生成函数 92 | 93 | - `SerialNoPool.generate_values(num:int) -> List[int]` 94 | 95 | 生成 num 个的 序列值。 96 | 97 | - `SerialNoPool.generate_labels(num:int) -> List[str]` 98 | 99 | 生成 num 个的 序列标签。 -------------------------------------------------------------------------------- /docs/guides/singleton.md: -------------------------------------------------------------------------------- 1 | # Singleton 模块 2 | 3 | > 模块: `borax.patterns.singleton` 4 | 5 | 该模块定义了 `MetaSingleton` 类,以元类方式实现单例模式。 6 | 7 | 若需要实现类 `A` 为单例模式,只需将其元类属性设置为 `MetaSingleton` 即可。 8 | 9 | ```python 10 | from borax.patterns.singleton import MetaSingleton 11 | 12 | class SingletonM(metaclass=MetaSingleton): 13 | pass 14 | ``` 15 | `SingletonM` 类的实例对象都共享相同状态和数据。如: 16 | 17 | ```python 18 | a = SingletonM() 19 | b = SingletonM() 20 | print(id(a) == id(b)) # True 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/guides/strings.md: -------------------------------------------------------------------------------- 1 | # 字符串工具 2 | 3 | > 模块 borax.strings 4 | 5 | 本模块提供了一系列有关字符串处理的工具函数。 6 | 7 | ## 命名风格 8 | 9 | 10 | 11 | - `camel2snake(s: str) -> str` 12 | 13 | 将罗马尼亚风格转化为驼峰命名法 14 | 15 | - `snake2camel(s: str) -> str` 16 | 17 | 将驼峰命名法转化为罗马尼亚风格 18 | 19 | ## 文件换行符 20 | 21 | 在不同的操作系统,文件换行符使用不同的字符表示。 22 | 23 | | 操作系统 | 换行符 | 24 | | -------- | ------ | 25 | | Windows | \r\n | 26 | | Linux | \n | 27 | 28 | `borax.strings.FileEndingUtil` 提供了处理不同换行符的转换函数。 29 | 30 | - `FileEndingUtil.windows2linux(content: bytes) -> bytes` 31 | - `FileEndingUtil.linux2windows(content: bytes) -> bytes` 32 | 33 | 将 content 中的换行符进行转化。content 必须是 bytes 类型。 -------------------------------------------------------------------------------- /docs/guides/tree.md: -------------------------------------------------------------------------------- 1 | # tree 模块 2 | 3 | > 模块: `borax.structure.tree` 4 | 5 | > Added in v1.1.4 6 | 7 | ## 功能 8 | 9 | `tree.pll2cnl` 能够将易于存储的 *Parent线性数组* 转化为 *Children嵌套数组* 形式。 10 | 11 | 12 | Parent线性数组(ParentLinearList)格式: 13 | ```json 14 | [ 15 | {"id": 1, "name": "node1", "parent": null}, 16 | {"id": 2, "name": "child1", "parent": 1}, 17 | {"id": 3, "name": "child2", "parent": 1}, 18 | {"id": 4, "name": "node2", "parent": null}, 19 | {"id": 5, "name": "child3", "parent": 5} 20 | ] 21 | ``` 22 | 23 | 24 | Children嵌套数组(ChildrenNestedList) 格式: 25 | 26 | ```json 27 | [ 28 | { 29 | "name": "node1", "id": 1, 30 | "children": [ 31 | { "name": "child1", "id": 2 }, 32 | { "name": "child2", "id": 3 } 33 | ] 34 | }, 35 | { 36 | "name": "node2", "id": 4, 37 | "children": [ 38 | { "name": "child3", "id": 5 } 39 | ] 40 | } 41 | ] 42 | ``` 43 | 44 | ## API 45 | 46 | 该模块只有一个函数 pll2cnl ,函数签名 47 | 48 | 49 | ```python 50 | def pll2cnl( 51 | nodelist, 52 | *, 53 | id_field='id', 54 | parent_field='parent', 55 | root_value=None, 56 | children_field='children', 57 | flat_fields=None, 58 | extra_fields=None, 59 | extra_key=None 60 | 61 | ): 62 | pass 63 | ``` 64 | 65 | > pll2cnl 的全称为 parent-linear-list-to-children-nested-list 。 66 | 67 | 参数定义如下: 68 | 69 | | 属性 | 类型 | 描述 | 其他 | 70 | | ------ | ------ | ------ | ------ | 71 | | id_field | `str` | 主键字段名称 | | 72 | | parent_field | `str` | 父节点字段名称 | | 73 | | root_value | `Any` | 根节点的主键值 | 通常取值 `None`、`-1` 等 | 74 | | children_field | `str` | 子节点字段名称 | | 75 | | flat_fields | `list` | 同级字段列表 | | 76 | | extra_key | `str` | 额外数据的键名称 | | 77 | | extra_fields | `list` | 其他字段列表 | | 78 | 79 | 其他参数要求: 80 | 81 | - `id_field`,`children_field`, `extra_key`, `flat_fields` 必须选取不同的值。 82 | 83 | 一个节点的通用格式如下: 84 | 85 | ``` 86 | { 87 | : , 88 | :{ 89 | : , 90 | : , 91 | ... 92 | } 93 | :, 94 | :, 95 | ... 96 | :[ 97 | , 98 | ... 99 | ] 100 | } 101 | ``` 102 | 103 | 例如对于配置 `{"extra_key": "extra", extra_fields":"name"}` ,上述节点将输出为以下格式: 104 | 105 | ```json 106 | { 107 | "id": 0, 108 | "extra":{ 109 | "name":"A" 110 | } 111 | } 112 | ``` 113 | 114 | ## 应用场景 115 | 116 | 可作为相关插件的远程数据构建工具,包括: 117 | 118 | - [jqTree](http://mbraak.github.io/jqTree/) 119 | - [jsTree](https://www.jstree.com/) 120 | - [ECharts 旭日图/矩形树图/树图](http://echarts.baidu.com/) 121 | 122 | -------------------------------------------------------------------------------- /docs/guides/ui.md: -------------------------------------------------------------------------------- 1 | # UI 模块 2 | 3 | > 模块: `borax.ui` 4 | 5 | ## aiotk:异步支持 6 | 7 | 使用方法: 8 | 9 | ```python 10 | import tkinter as tk 11 | import asyncio 12 | 13 | from borax.ui.aiotk import run_loop 14 | 15 | class App(tk.Tk): 16 | def __init__(self): 17 | super().__init__() 18 | self.title('Async Demo') 19 | 20 | app = App() 21 | loop = asyncio.get_event_loop() 22 | loop.run_until_complete(run_loop(app)) 23 | ``` -------------------------------------------------------------------------------- /docs/images/app_borax_calendar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/docs/images/app_borax_calendar.png -------------------------------------------------------------------------------- /docs/images/app_festival_creator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/docs/images/app_festival_creator.png -------------------------------------------------------------------------------- /docs/images/calendar_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/docs/images/calendar_frame.png -------------------------------------------------------------------------------- /docs/images/donation-wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/docs/images/donation-wechat.png -------------------------------------------------------------------------------- /docs/images/festival_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/docs/images/festival_table.png -------------------------------------------------------------------------------- /docs/images/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/docs/images/image1.png -------------------------------------------------------------------------------- /docs/posts/lunardate-development.md: -------------------------------------------------------------------------------- 1 | # 农历开发笔记 2 | 3 | ## 0 概述 4 | 5 | 到2019年1月为止,关于农历的主题,github/PyPI 上已经有非常多的代码项目,语言有C、Java、Python等,具体的思路也不一样。综合来看,这些库有的功能单一,只覆盖某几个方面;有的已经很久没有更新了,主要是农历信息已在多年之前就采集完成,但是对于一些最新的数据修正未能及时涵盖;也有的在代码层面没有很好的适用最新的 Python 语言特性。 6 | 7 | 基于此,笔者利用收集整理的一些技术资料开发出了 Borax.Lunardate这个库,主要的目标和特点有: 8 | 9 | - **完整的农历信息** 10 | 11 | 在开发过程中笔者收集网络上的几个重要农历数据,包含了干支、生肖、节气等事项,并同时将它们作为数据验证的参考标准。 12 | 13 | - **功能完备** 14 | 15 | Borax.Lunardate 库分为三个部分:1) 基于 `LunarDate` 的农历日期表示;2)类似于 `datetime.strftime` 的字符串格式系统;3) 一些常用的农历工具接口。 16 | 17 | 其中第2,3部分是网络上的农历库比较少涉及的,Borax-Lunardate 在这一方面非常有优势的。 18 | 19 | - **对标datetime** 20 | 21 | 在模块/类层面的组织和分类上,Borax.Lunardate 对标标准库的 `datetime` 和 `calendar` 模块,实现了这两个模块中与农历日期相联系的方法,`LunarDate` 和 `date` 类有许多相同的特性,包括不可变类、可比较性、时间加减等。 甚至有些命名也是一样的,比如 `strftime` 方法。 22 | 23 | ## 1 农历基本知识 24 | 25 | 农历是我国的传统历法,依据太阳和月球位置的精确预报以及约定的日期编排规则编排日期,并以传统命名方法表述日期。 26 | 27 | 2017年,我国已经颁布了国家推荐性标准 [《GB/T 33661-2017 农历的编算和颁行》]([国家标准|GB/T 33661-2017 (samr.gov.cn)](http://openstd.samr.gov.cn/bzgk/gb/newGbInfo?hcno=E107EA4DE9725EDF819F33C60A44B296))。 28 | 29 | ### 1.1 编排规则 30 | 31 | 农历属于一种阴阳合历,基本规则如下:其年份分为平年和闰年。平年为十二个月;闰年为十三个月。月份分为大月和小月,大月三十天,小月二十九天。一年中哪个月大,哪个月小,可由“置闰规则”计算决定。 32 | 33 | > 若从某个农历十一月开始到下一个农历十一月(不含)之间有13个农历月,则需要置闰。置闰规则为:去其中最先出现的一个不包含中气的农历月为农历闰月。 34 | 35 | 除此之外,还有生肖纪年、干支纪年、二十四节气等。 36 | 37 | ### 1.2 表示方法 38 | 39 | 农历日期通常有以下几种表示方法: 40 | 41 | - 农历乙未年正月初一 42 | - 农历牛年闰五月十一 43 | - 农历甲午年七月庚戌日 44 | - 公元2016年农历丙申年十一月廿九 45 | 46 | ### 1.3 二十四节气 47 | 48 | 一个回归年内24个太阳地心视黄经等于15度的整数倍的时刻的总称,每个时刻成为一个节气。太阳每年运行360度,共经历二十四个节气,分别为立春(315度)、雨水(330度)、惊蛰(345度)、春分(0度、360度)、清明(15度)、谷雨(30度)、立夏(45度)、小满(60度)、芒种(75度)、夏至(90度)、小暑(105度)、大暑(120度)、立秋(135度)、处暑(150度)、白露(165度)、秋分(180度)、寒露(195度)、霜降(210度)、立冬(225度)、小雪(240度)、大雪(255度)、冬至(270度)、小寒(285度)、大寒(300度)。可以通过下面的儿歌记忆这些节气。 49 | 50 | ``` 51 | 春雨惊春清谷天, 52 | 夏满芒夏暑相连, 53 | 秋处露秋寒霜降, 54 | 冬雪雪冬小大寒, 55 | 每月两节不变更, 56 | 最多相差一两天 57 | ``` 58 | 59 | 2016年11月30日,中国“二十四节气”被正式列入联合国教科文组织人类非物质文化遗产代表作名录。 60 | 61 | ## 2 数据结构 62 | 63 | ### 2.1 大小月和闰月 64 | 65 | 从香港天文台网站可以获取1900 - 2100年的农历信息,每一天包含公历日期、农历日期、星期、节气四项基本信息。日期范围的基本信息如下表: 66 | 67 | | 项目 | 起始日 | ... | 2100年 | 2101年 | ... | 截止日 | 68 | | ------ | ------------------ | ---- | ------------------ | ---------------- | ---- | ------------------ | 69 | | 公历 | 1990年1月31日 | ... | 2100年12月31日 | 2101年1月1日 | ... | 2101年1月28日 | 70 | | 农历 | 1900年正月初一 | ... | 2100年十二月初一 | 2100年十二月初二 | ... | 2100年十二月二十九 | 71 | | offset | 0 | ... | 73383 | 73384 | ... | 73411 | 72 | | 干支 | 庚午年丙子月壬辰日 | ... | 庚申年戊子月丁未日 | - | ... | - | 73 | 74 | 具体到一个农历年中,从中可以看出以下几点信息: 75 | 76 | - 每个月有多少天;哪些是大月(30天),哪些是小月(29天) 77 | - 本年是否有闰月;如果有,是哪个月份 78 | 79 | 如何使用精炼的数据结构表述这些信息,是一个重要的前提,主要要求算法简单、内存占用少。网上有许多种方式,一种比较通行的做法是使用5字节的数据,高3位总是“000”,实际使用的低17位二进制。 80 | 81 | | 字段 | 闰月大小标志 | 月份大小标志 | 闰月月份 | 82 | | ---------- | ------------ | -------------------- | ---------- | 83 | | 大小 | 4b | 12b | 4b | 84 | | 2017年示例 | 0001 | 0101 0001 0111 | 0110 | 85 | | 描述 | 本年有闰月 | 2,4,8,10,11,12为大月 | 六月是闰月 | 86 | | 2019年示例 | 0000 | 1010 1001 0011 | 0000 | 87 | | 描述 | 无闰月 | 1,3,5,8,11,12为大月 | 无闰月 | 88 | 89 | 综上所述,2017年信息可以使用 0x15176 表示;2019年信息可使用 0x0a930 表示。 90 | 91 | ### 2.2 节气的数据结构 92 | 93 | 二十四节气开始的日期,与通用的公历几乎一致,最多相差一两天,因为是按照地球一年绕太阳公转一周作为依据。比如小寒通常落在在1月5-7日,立春落在2月3-5日,冬至落在12月21-23日。即每个月都会有2个节气,1月只能有小寒、大寒这两个节气。 94 | 95 | **36位字符串表示法** 96 | 97 | 98 | 构建两个含有24元素的数组, 99 | 100 | 第一个数组以小寒为第1个节气重新排列这24个节气。 101 | 102 | ``` 103 | 小寒, 大寒, 立春, 雨水, 惊蛰, 春分, 清明, 谷雨, 立夏, 小满, 芒种, 夏至, 104 | 小暑, 大暑, 立秋, 处暑, 白露, 秋分, 寒露, 霜降, 立冬, 小雪, 大雪, 冬至 105 | ``` 106 | 107 | 第二个数组表示对应节气对应的日期数字。 108 | 109 | ``` 110 | 6 20 4 19 6 21 5 20 6 21 6 22 7 23 8 23 8 23 9 24 8 23 7 22 111 | ``` 112 | 113 | 结合这两个数组,可记录在一个公历年中,二十四个节气分别是在哪一天。比如上述的24个数字可解释为:1月6日是小寒、1月20日是大寒...12月7日是大雪、12月22日是冬至。 114 | 115 | 在 Python 语言层面,可以使用字符串(基本数据类型)代替上述数组(复合数据类型),即`"620419621520621622723823823924823722"` ,需要36位字符存储。 116 | 117 | 解析表中数据的 Python 代码实现如下: 118 | 119 | ```python 120 | def parse_term(year_info): 121 | result = [] 122 | for i in range(0, 36, 3): 123 | s = year_info[i:i + 3] 124 | result.extend([int(s[0]), int(s[1:3])]) 125 | return result 126 | ``` 127 | 128 | 129 | 130 | **30位字符串表示法** 131 | 132 | [jjonline/calendar.js](https://github.com/jjonline/calendar.js) 提供了一种用更为简单的表示方法:利用十六进制压缩数字的位数,进一步简化为30位的字符串。具体计算过程如下: 133 | 134 | ``` 135 | 9778397bd097c36b0b6fc9274c91aa # 按长度5分割,共6组 136 | 137 | 97783 97bd0 97c36 b0b6f c9274 c91aa # 转化为十进制 138 | 139 | 620419 621520 621622 723823 823924 823722 # 按长度1,2,1,2细分 140 | 141 | 6 20 4 19 6 21 5 20 6 21 6 22 7 23 8 23 8 23 9 24 8 23 7 22 142 | ``` 143 | 144 | 使用 Python代码实现上述算法如下: 145 | 146 | ```python 147 | def parse_term(term_info): 148 | values = [str(int(term_info[i:i + 5], 16)) for i in range(0, 30, 5)] 149 | term_day_list = [] 150 | for v in values: 151 | term_day_list.extend([ 152 | int(v[0]), int(v[1:3]), int(v[3]), int(v[4:6]) 153 | ]) 154 | return term_day_list 155 | ``` 156 | 157 | **24位字符串** 158 | 159 | > 从 Borax v1.2.0 开始使用算法。 160 | 161 | 统计1900-2100年之前节气日期统计可知,中气的日期都是在18-24日之间,这些均为两位数,可以通过线性变化转为一位数的数字,结合月份特点,可以通过减去一个固定偏移量15就是比较好的选择。 162 | 163 | 同样的按照上述处理,具体过程如下: 164 | 165 | ``` 166 | 654466556667788888998877 # 按长度1分割 167 | 168 | 6 5 4 4 6 6 5 5 6 6 6 7 7 8 8 8 8 8 9 9 8 8 7 7 # 增加偏移量,奇位置为0,偶位置为15 169 | 170 | 6 20 4 19 6 21 5 20 6 21 6 22 7 23 8 23 8 23 9 24 8 23 7 22 171 | ``` 172 | 173 | 同样的使用Python 代码如下,和30位表示法相比,更为简单直接。 174 | 175 | ```python 176 | def parse_term_days(term_info): 177 | return [int(c) + [0, 15][i % 2] for i, c in enumerate(term_info)] 178 | ``` 179 | 180 | 181 | ## 3 数据处理 182 | 183 | 本模块的数据和算法参考自项目 [jjonline/calendar.js](https://github.com/jjonline/calendar.js) 184 | 185 | v1.2 发布后,将针对原始数据进行一次校验 186 | 187 | 188 | 提供了数据源,说明 189 | 190 | 191 | - 微软数据源比对使用于项目 [Lunar-Solar-Calendar-Converter](https://github.com/isee15/Lunar-Solar-Calendar-Converter) 的相关代码 192 | 193 | 194 | | 月份 | Borax-v1.2 | 微软农历 | 香港天文台 | 初步操作 | | 195 | | ----------- | ---------- | -------- | ---------- | --------------------- | ---- | 196 | | 1933年闰5月 | 29 | 30 | 30 | 改(0x6e95 -0x16a95) | | 197 | | 1933年6月 | 30 | 29 | 29 | 改() | | 198 | | 1996年5月 | 29 | 30 | 30 | 改(0x055c0 - 0x05ac0) | | 199 | | 1996年6月 | 30 | 29 | 29 | 改 | | 200 | | 1996年7月 | 29 | 30 | 30 | 改 | | 201 | | 1996年8月 | 30 | 29 | 29 | 改 | | 202 | | 2060年3月 | 30 | 29 | 29 | 改(0x0a2e0 - 0x092e0) | | 203 | | 2060年4月 | 29 | 30 | 30 | 改 | | 204 | | 2089年7月 | 29 | 30 | 29 | 0x0d160 (0x0d260) | | 205 | | 2089年8月 | 30 | 29 | 30 | | | 206 | | 2097年6月 | 29 | 30 | 29 | 0x0a2d0 (0x0a4d0) | | 207 | | 2097年7月 | 30 | 29 | 30 | | | 208 | 209 | ## 4 参考资料 210 | 211 | - [香港天文台农历信息](http://www.hko.gov.hk/gts/time/conversion.htm) 212 | - [“农历”维基词条](https://en.wikipedia.org/wiki/Chinese_calendar) 213 | - [jjonline/calendar.js](https://github.com/jjonline/calendar.js) 214 | - [lidaobing/python-lunardate](https://github.com/lidaobing/python-lunardate) 215 | -------------------------------------------------------------------------------- /docs/release-note/v350.md: -------------------------------------------------------------------------------- 1 | # v3.5发布日志 2 | 3 | > 发布日期:2021年11月15日 4 | 5 | ## 1 python版本支持 6 | 7 | Borax 3.5 支持python3.5 ~ 3.10。 8 | 9 | 从 Borax3.6开始将移除对python3.5的支持。 10 | 11 | ## 2 festivals2新版节日库 12 | 13 | `festivals2` 和 `festivals` 相比,功能也更为丰富。 14 | 15 | `festivals.DateSchema` 和 `festivals2.Festival` 都是节日的基类,不可实例化。 16 | 17 | | DateSchema | Festival | 备注 | 18 | | ------ | ----- | ------ | 19 | | DateSchema.match | Festival.is_ | | 20 | | DateSchema.resolve/resolve_solar | Festival.at | at增加month参数| 21 | | DateSchema.countdown | Festival.countdown | Festival.countdown返回值新增相差天数 | 22 | | - | Festival.get_one_day | | 23 | | - | Festival.list_days | | 24 | 25 | ## 3 废弃LunarDate序列化 26 | 27 | 随着 festivals2 的引入,节日日期序列化的实现进行了改变,类 `LunarDate` 不再继承 `EncoderMixin`。下面的调用方式将被废弃: 28 | 29 | ```python 30 | 31 | from borax.calendars import LunarDate 32 | 33 | ld = LunarDate(2021, 8, 15) 34 | print(ld.encode()) # 202108150 35 | 36 | ld2 = LunarDate.decode('202102010') 37 | print(ld2) # LunarDate(2021, 2, 1) 38 | ``` 39 | 40 | 新版将使用下列的方式,即通过 `WrappedDate` 对象作为转化的中间桥梁。 41 | 42 | ```python 43 | from borax.calendars import LunarDate 44 | from borax.calendars.festivals2 import WrappedDate 45 | 46 | ld = LunarDate(2021, 8, 15) 47 | print(WrappedDate(ld).encode()) # '1202108150' 48 | 49 | # 根据传入WrappedDate的日期类型决定按公历或农历方式编码 50 | sd = ld.to_solar_date() 51 | print(WrappedDate(sd).encode()) # '0202109210' 52 | 53 | 54 | wd = WrappedDate.decode('1202102010') 55 | print(wd.lunar) # LunarDate(2021, 2, 1) 56 | ``` 57 | 58 | ## 4 移除的模块和函数 59 | 60 | - 模块 `borax.calendars.festivals` 61 | - 模块 `borax.serialize.bjson` 62 | - 函数 `borax.serialize.cjson.to_serializable` 63 | - 函数 `borax.datasets.join_.old_join` 64 | - 函数 `borax.datasets.join_.old_join_one` 65 | - 模块 `borax.finance` -------------------------------------------------------------------------------- /docs/release-note/v356.md: -------------------------------------------------------------------------------- 1 | # v3.5.6发布日志 2 | 3 | > 发布日期:2022年7月3日 4 | 5 | ## 概述 6 | 7 | v3.5.6是一个引入若干个特性的版本,这些特性不会对已有逻辑进行破坏性更新。主要包含: 8 | 9 | - 新增 `LunarDate.strptime` 反向解析函数 10 | - `LunarDate` 农历日期新增格式化修饰符 `%c` 11 | - `WrappedDate` 新增`simple_str` 简化显示方法。如:“2022年5月7日(四月初七)” 。 12 | - 星期型节日 `WeekFestival` 新增倒数序号和每月频率 13 | - `FestivalLibrary` 功能增强 14 | - 新增新的节日库 `ext1` 15 | 16 | 17 | ## 1 农历 18 | 19 | ### 日期反向解析 strptime 20 | 21 | 从文本字符串解析农历日期 `LunarDate` 对象。 22 | 23 | ```python 24 | from borax.calendars.lunardate import LunarDate 25 | 26 | ld = LunarDate.strptime('二〇二二年四月初三', '%Y年%L%M月%D') 27 | print(ld) # LunarDate(2022, 4, 3, 0) 28 | ``` 29 | 30 | ### 新的格式化修饰符:%c 31 | 32 | 新的修饰符 `%c` 显示中文月日信息,不显示年份信息,等效于 `%L%N月%D`。 33 | 34 | ```python 35 | from borax.calendars.lunardate import LunarDate 36 | 37 | print(LunarDate(2022, 4, 3).strftime('%c')) # '四月初三' 38 | print(LunarDate(2020, 4, 22).strftime('%c')) # '闰四月廿二' 39 | ``` 40 | 41 | 该修饰符通常用于和公历日期同时显示的场景,在该种场景下,农历年份总是很容易推导。 42 | 43 | ```python 44 | from borax.calendars.festivals2 import WrappedDate 45 | 46 | wd = WrappedDate(LunarDate(2022, 3, 4)) 47 | print(f'{wd.solar}({wd.lunar:%c})') # '2022-04-04(三月初四)' 48 | ``` 49 | 50 | 51 | ### 干支工具 52 | 53 | 54 | 新增干支序号和干支文字的转化。 55 | 56 | 57 | ``` 58 | 0 甲子 59 | 1 乙丑 60 | ... 61 | 59 癸亥 62 | ``` 63 | 示例 64 | 65 | 66 | ``` 67 | >>>TextUtils.offset2gz(0) 68 | "甲子" 69 | >>>TextUtils.gz2offset("癸亥") 70 | 59 71 | >>>TextUtils.gz2offset("甲丑") 72 | ValueError("Invalid gz string:甲丑.") 73 | ``` 74 | 75 | ## 2 星期型节日 76 | 77 | ### 序号支持倒数 78 | 79 | 使用场景:国际麻风节 80 | 81 | ```python 82 | # 每年1月的最后一个星期日 83 | mafeng = WeekFestival(month=1, index=-1, week=calendar.SUNDAY, name='国际麻风节') 84 | ``` 85 | 在编码序列化上,index字段(左起第四五位)中第四位表示正向或倒数(取值1)。 86 | 87 | 88 | ```text 89 | 206036 父亲节 6月第3个星期日 90 | 201116 国际麻风节 1月第倒数第1个星期日(week=6) 91 | ``` 92 | 93 | ### 支持每月属性 94 | 95 | 允许 `WeekFestival` 中 month 取值为0,表示“每月”频率。 96 | 97 | ```python 98 | last_sunday_monthly = WeekFestival(month=0, index=-1, week=calendar.SUNDAY) 99 | ``` 100 | 101 | ### 综合示例 102 | 103 | `WeekFestival`支持以下的定义方式: 104 | 105 | ```python 106 | fs2 = WeekFestival(month=4, index=1, week=calendar.SUMDAY) # 每年4月第1个星期日 107 | 108 | fs3 = WeekFestival(month=0, index=1, week=calendar.SUMDAY) # 每月第1个星期日 109 | 110 | fs4 = WeekFestival(freq=FreqConst.MONTHLY, index=1, week=calendar.SUNDAY) # 每月第1个星期日 111 | 112 | fs5 = WeekFestival(feq=FreqConst.MONTHLY, index=-1, week=calendar.SUNDAY) # 每月最后一个星期日 113 | ``` 114 | 115 | ## 3 节气型节日 116 | 117 | 新增支持基于节气的某些节日。“初伏”为“夏至起的第三个庚日”,可定义为: 118 | 119 | ```python 120 | tf = TermFestival('夏至', nth=3, day_gz='庚') 121 | ``` 122 | 123 | 类似的节日有:三伏天、九九天、入梅、出梅。 124 | 125 | ## 4 节日属性Festival 126 | 127 | ### 新增分类属性 128 | 129 | 新增 `Festival.catalog` 属性,显示节日对象的分类属性。可用的属性值包括: 130 | 131 | | 属性 | 描述 | 132 | | ---- | ---- | 133 | | life | 诞辰逝世纪念日 | 134 | | event | 事件纪念日,机构成立日 | 135 | | other | 其他型节日 | 136 | | public | 公共节日 | 137 | | tradition | 传统节日 | 138 | 139 | ### 导出csv文件 140 | 141 | 使用 `to_csv` 函数可以导出到文件。 142 | 143 | ```python 144 | fl = FestivalLibrary.load_builtin() 145 | fl.to_csv('my_festivals.csv') 146 | ``` 147 | 148 | ## 5 内置数据库 149 | 150 | 新的节日库 *ext1* 包含了许多国际、行业性节日,数目在360+,使用下列代码加载。 151 | 152 | ```python 153 | fl = FestivalLibrary.load_builtin('ext1') 154 | ``` 155 | 156 | 示例数据 157 | 158 | ```text 159 | 001010,元旦,public 160 | 001100,中国人民警察节,public 161 | 001260,国际海关日,public 162 | ... 163 | 206025,文化和自然遗产日,public 164 | 207015,国际合作节,public 165 | 209025,世界急救日,public 166 | ``` 167 | 168 | -------------------------------------------------------------------------------- /docs/release-note/v400.md: -------------------------------------------------------------------------------- 1 | # v4.0.0发布日志 2 | 3 | > 发布日期:2022年11月15日 4 | 5 | ## 概述 6 | 7 | v4.0.0 是一个重大的版本更新。 8 | 9 | ## 1 python版本要求 10 | 11 | 从v4.0.0开始,Borax要求 python 版本在 3.7 以上。 12 | 13 | | borax 版本 | python版本 | 14 | | ------ | ------ | 15 | | 4.0 | 3.7 - 3.11 | 16 | | 3.x | 3.5+ | 17 | 18 | ## 2 日历界面库 19 | 20 | Borax v4.0新增基于 `tkinter.ttk` 的界面组件库,具体参见 [《日历界面库》](/guides/festivals2-ui)。 21 | 22 | 23 | | 组件 | 类 | 24 | | ------------ | ------------------------------------- | 25 | | 日历组件 | borax.calendars.ui.CalendarFrame | 26 | | 节日表格组件 | borax.calendars.ui.FestivalTableFrame | 27 | 28 | ## 3 日期选择器 29 | 30 | `borax.calendars.datepicker` 模块实现了简单的日期选择器。 31 | 32 | ```python 33 | import tkinter as tk 34 | from tkinter import ttk 35 | 36 | from borax.calendars.datepicker import ask_date 37 | 38 | 39 | def main(): 40 | root = tk.Tk() 41 | root.title('日期选择器') 42 | root.resizable(False, False) 43 | date_var = tk.StringVar() 44 | entry = ttk.Entry(root, textvariable=date_var) 45 | entry.pack(side='left') 46 | 47 | def askdate(): 48 | wd = ask_date() 49 | print(wd) 50 | if wd: 51 | date_var.set(wd) 52 | 53 | btn = ttk.Button(root, text='点击', command=askdate) 54 | btn.pack(side='left') 55 | root.mainloop() 56 | 57 | 58 | if __name__ == '__main__': 59 | main() 60 | ``` 61 | 62 | ## 4 节日库 `FestivalLibrary` 支持编辑功能 63 | 64 | 在 v4.0.0 中,`FestivalLibrary` 类新增四个编辑节日条目的函数: 65 | 66 | - delete_by_indexes:按节日的索引删除节日 67 | - filter_:按照条件过滤节日条目,保留符合参数条件的节日 68 | - exclude_:按照条件过滤条目,删除符合参数条件的节日 69 | - sort_by_countdown:按照倒计天数排序 70 | 71 | 以上三个函数均为”原地修改“的,如果需要保留之前的数据,使用 `backup_lib = lib[:]` 先行备份。 72 | -------------------------------------------------------------------------------- /docs/release-note/v410.md: -------------------------------------------------------------------------------- 1 | # v4.1.0发布日志 2 | 3 | > 发布时间:2024年1月31日 4 | 5 | 6 | 7 | ## 1 项目开发 8 | 9 | 从 4.1.0 开始,Borax 在项目开发构建上有重大变更,具体包括: 10 | 11 | - **Borax不再支持 python3.7和python3.8,最低版本为3.9** 12 | - 本地开发环境更新至3.11 13 | - 使用 *pyproject.toml* 取代原有的 *setup.py* 和 *setup.cfg* 文件。 14 | - 更新大量开发依赖库( *requirements_dev.txt* )的版本。 15 | 16 | 17 | 18 | ## 2 日历应用 19 | 20 | Borax 提供了一个基于 tkinter 的日历应用程序,该日历应用包含了一些常见的功能: 21 | 22 | - 万年历显示 23 | - 日期计算相关工具 24 | - 查看节日、节气、干支信息 25 | - 创建和导出节日源 26 | 27 | 在安装 Borax 之后,使用 `python -m borax.capp` 启动该界面程序 。 28 | 29 | ## 3 其他功能 30 | 31 | Borax 4.1.0 主要更新了 `borax.calendars.festivals2` 模块的功能。 32 | 33 | ### 3.1 WrappedDate 34 | 35 | `WrappedDate.solar` 和 `WrappedDate.lunar` 属性修改为 **只读属性,不可写入** 。 36 | 37 | ### 3.2 Festival 38 | 39 | `Festival` 新增 `code` 属性,表示节日的编码,该属性为惰性属性,使用 `cached_property` 装饰。 40 | 41 | ### 3.3 FestivalLibrary 42 | 43 | 新增 `FestivalLibrary.load` 函数,这是 `load_file` 和 `load_builtin` 的混合接口 。 44 | 45 | ```python 46 | fl1 = FestivalLibrary.load('basic') 47 | 48 | fl2 = FestivalLibrary.load('c:\\users\\samuel\\festival_data\\my_festivals.csv') 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | site_name: Borax 2 | theme: 3 | icon: 4 | logo: material/calendar 5 | name: material 6 | palette: 7 | primary: pink 8 | features: 9 | - navigation.tabs 10 | - navigation.sections 11 | - navigation.footer 12 | validation: 13 | links: 14 | absolute_links: ignore 15 | unrecognized_links: ignore 16 | extra: 17 | social: 18 | - icon: fontawesome/brands/github 19 | link: https://github.com/kinegratii/borax 20 | - icon: fontawesome/brands/python 21 | link: https://pypi.org/project/borax 22 | copyright: Copyright © 2018 - 2025 Samuel.Yan 23 | markdown_extensions: 24 | - pymdownx.highlight 25 | - pymdownx.superfences 26 | - mdx_truly_sane_lists 27 | nav: 28 | - 首页: README.md 29 | - 日历应用: guides/borax_calendar_app.md 30 | - 农历与节日: 31 | - 农历: guides/lunardate.md 32 | - 节日: guides/festivals2.md 33 | - 节日集合库: guides/festivals2-library.md 34 | - 编码序列化: guides/festivals2-serialize.md 35 | - 生日: guides/birthday.md 36 | - 工具类: guides/calendars-utils.md 37 | - 日历界面库: guides/festivals2-ui.md 38 | - 数据结构与数字: 39 | - 数据连接(Join): guides/join.md 40 | - 列选择器(fetch): guides/fetch.md 41 | - 树形结构: guides/tree.md 42 | - cjson: guides/cjson.md 43 | - 中文数字: guides/numbers.md 44 | - 百分数: guides/percentage.md 45 | - 其他模块: 46 | - Django-Choices: guides/choices.md 47 | - 字符串: guides/strings.md 48 | - 单例模式: guides/singleton.md 49 | - 序列号池: guides/serial_pool.md 50 | - tkui: guides/ui.md 51 | - 已废弃模块: 52 | - bjson: guides/bjson.md 53 | - 节日库(旧版): guides/festival.md 54 | - 序列号池(旧版): guides/serial_generator.md 55 | - 文章: 56 | - 农历与节日: guides/festivals2-usage.md 57 | - 农历开发笔记: posts/lunardate-development.md 58 | - 开发&更新日志: 59 | - 更新日志: changelog.md 60 | - Capp更新日志: capp_changelog.md 61 | - 开发日志: develop_note.md 62 | - 发布日志: 63 | - v4.1.0: release-note/v410.md 64 | - v4.0.0: release-note/v400.md 65 | - v3.5.6: release-note/v356.md 66 | - v3.5.0: release-note/v350.md -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "borax" 7 | authors = [ 8 | { name = "kinegratii", email = "zhenwei.yan@hotmail.com" }, 9 | ] 10 | description = "A tool collections.(Chinese-Lunar-Calendars/Python-Patterns)" 11 | readme = "README.md" 12 | requires-python = ">=3.9" 13 | keywords = ["chinese lunar calendar", "python tool"] 14 | license = { text = "MIT License" } 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Programming Language :: Python :: 3.13", 23 | "Programming Language :: Python :: 3 :: Only", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: MIT License", 26 | "Topic :: Software Development :: Libraries", 27 | "Topic :: Utilities", 28 | 'Operating System :: OS Independent' 29 | ] 30 | dynamic = ["version"] 31 | 32 | [project.urls] 33 | Homepage = "https://github.com/kinegratii/borax" 34 | Documentation = "https://borax.readthedocs.io/zh_CN/latest/" 35 | Repository = "https://github.com/kinegratii/borax" 36 | 37 | [tool.setuptools] 38 | include-package-data = true 39 | 40 | [tool.setuptools.dynamic] 41 | version = { attr = "borax.__version__" } 42 | 43 | [tool.coverage.run] 44 | omit = [ 45 | "borax\\ui\\*.py", 46 | "borax\\capp\\*.py", 47 | "borax\\apps\\*.py", 48 | "borax\\calendars\\datepicker.py", 49 | "borax\\calendars\\ui.py" 50 | ] 51 | [tool.coverage.report] 52 | exclude_lines = [ 53 | "def __repr__", 54 | "raise AssertionError", 55 | "raise NotImplementedError", 56 | "if 0:", 57 | "if __name__ == .__main__.:", 58 | "def print_(self):" 59 | ] 60 | [tool.flake8] 61 | ignore = ["E743", "E501"] 62 | max-line-length = 120 63 | max-complexity = 25 64 | exclude = [".git", "__pycache__", "build", "dist"] -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | nose2~=0.14 2 | coverage~=7.4 3 | flake8~=6.1 4 | mccabe~=0.6 5 | wheel~=0.42 6 | setuptools~=65.0 7 | build~=1.0 8 | Flake8-pyproject~=1.2 -------------------------------------------------------------------------------- /requirements_doc.txt: -------------------------------------------------------------------------------- 1 | mkdocs-material==9.5.3 2 | mdx-truly-sane-lists==1.3 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | long_description = file: README.md 3 | long_description_content_type = text/markdown 4 | project_urls = 5 | Homepage = https://borax.readthedocs.io/zh_CN/latest/ 6 | Source Code = https://github.com/kinegratii/borax 7 | Bug Tracker = https://github.com/kinegratii/borax/issues 8 | 9 | [nosetests] 10 | traverse-namespace = 1 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import pathlib 3 | import re 4 | 5 | from setuptools import setup, find_packages 6 | 7 | here = pathlib.Path(__file__).parent 8 | 9 | txt = (here / 'borax' / '__init__.py').read_text() 10 | version = re.findall(r"^__version__ = '([^']+)'\r?$", txt, re.M)[0] 11 | 12 | lib_classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python :: 3.9", 16 | "Programming Language :: Python :: 3.10", 17 | "Programming Language :: Python :: 3.11", 18 | "Programming Language :: Python :: 3.12", 19 | "Programming Language :: Python :: 3 :: Only", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: MIT License", 22 | "Topic :: Software Development :: Libraries", 23 | "Topic :: Utilities", 24 | 'Operating System :: OS Independent' 25 | ] 26 | 27 | with open('README.md', encoding='utf8') as f: 28 | long_description = f.read() 29 | 30 | setup( 31 | name='borax', 32 | version=version, 33 | python_requires='>=3.9', 34 | packages=find_packages(exclude=['tests']), 35 | include_package_data=True, 36 | license='MIT', 37 | author='kinegratii', 38 | author_email='zhenwei.yan@hotmail.com', 39 | classifiers=lib_classifiers, 40 | description='A tool collections.(Chinese-Lunar-Calendars/Python-Patterns)', 41 | long_description=long_description, 42 | long_description_content_type='text/markdown', 43 | ) 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kinegratii/borax/01cbbbf7ce33ee22d23c462c525d07e1320e67a7/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_birthday.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import date 3 | 4 | from borax.calendars.birthday import nominal_age, actual_age_solar, actual_age_lunar, BirthdayCalculator 5 | from borax.calendars.festivals2 import WrappedDate 6 | from borax.calendars.lunardate import LunarDate 7 | 8 | 9 | class BirthdayCalculatorTestCase(unittest.TestCase): 10 | def test_solar_birthday(self): 11 | bc1 = BirthdayCalculator(date(2000, 3, 4)) # LunarDate(2000, 1, 29) 12 | 13 | result = bc1.calculate(date(2010, 3, 3)) 14 | self.assertEqual(9, result.actual_age) 15 | self.assertEqual(11, result.nominal_age) 16 | 17 | result1 = bc1.calculate(date(2010, 3, 4)) 18 | self.assertEqual(10, result1.actual_age) 19 | self.assertEqual(11, result1.nominal_age) 20 | 21 | result2 = bc1.calculate(LunarDate(2009, 12, 30)) 22 | self.assertEqual(10, result2.nominal_age) 23 | 24 | result3 = bc1.calculate(LunarDate(2010, 1, 1)) 25 | self.assertEqual(11, result3.nominal_age) 26 | 27 | def test_leap_feb(self): 28 | bc = BirthdayCalculator(date(2020, 2, 29)) 29 | result = bc.calculate(date(2021, 2, 28)) 30 | self.assertEqual(0, result.actual_age) 31 | result = bc.calculate(date(2021, 3, 1)) 32 | self.assertEqual(1, result.actual_age) 33 | 34 | def test_same_days(self): 35 | my_birthday = LunarDate(2004, 2, 2) 36 | this_day = date(2023, 1, 1) 37 | my_bc = BirthdayCalculator(my_birthday) 38 | same_day_list = my_bc.list_days_in_same_day(start_date=this_day) 39 | self.assertIn(WrappedDate(LunarDate(2023, 2, 2)), same_day_list) 40 | 41 | 42 | class NominalAgeTestCase(unittest.TestCase): 43 | def test_nominal_age(self): 44 | birthday = LunarDate(2017, 6, 16, 1) 45 | self.assertEqual(1, nominal_age(birthday, LunarDate(2017, 6, 21, 1))) 46 | self.assertEqual(1, nominal_age(birthday, LunarDate(2017, 12, 29))) 47 | self.assertEqual(2, nominal_age(birthday, LunarDate(2018, 1, 1))) 48 | 49 | 50 | class ActualAgeTestCase(unittest.TestCase): 51 | def test_nominal_age(self): 52 | self.assertEqual(13, actual_age_solar(date(2004, 3, 1), date(2017, 3, 1))) 53 | self.assertEqual(12, actual_age_solar(date(2004, 3, 1), date(2017, 2, 28))) 54 | 55 | self.assertEqual(2, actual_age_solar(date(2000, 2, 29), date(2003, 2, 28))) 56 | self.assertEqual(3, actual_age_solar(date(2000, 2, 29), date(2003, 3, 1))) 57 | 58 | self.assertEqual(3, actual_age_solar(date(2000, 2, 29), date(2004, 2, 28))) 59 | self.assertEqual(4, actual_age_solar(date(2000, 2, 29), date(2004, 2, 29))) 60 | self.assertEqual(4, actual_age_solar(date(2000, 2, 29), date(2004, 3, 1))) 61 | 62 | 63 | class MixedAgeTestCase(unittest.TestCase): 64 | def test_mixed(self): 65 | birthday = date(1983, 5, 20) 66 | self.assertEqual(23, actual_age_solar(birthday, today=date(2007, 5, 19))) 67 | self.assertEqual(24, actual_age_solar(birthday, today=date(2007, 5, 20))) 68 | self.assertEqual(24, actual_age_solar(birthday, today=date(2007, 5, 21))) 69 | 70 | # 71 | self.assertEqual(23, actual_age_lunar(birthday, today=date(2007, 5, 23))) 72 | self.assertEqual(24, actual_age_lunar(birthday, today=date(2007, 5, 24))) 73 | self.assertEqual(24, actual_age_lunar(birthday, today=date(2007, 5, 25))) 74 | 75 | self.assertEqual(25, nominal_age(birthday, today=date(2007, 5, 23))) 76 | self.assertEqual(25, nominal_age(birthday, today=date(2007, 5, 24))) 77 | self.assertEqual(25, nominal_age(birthday, today=date(2007, 5, 25))) 78 | -------------------------------------------------------------------------------- /tests/test_calendars.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import date, timedelta 3 | 4 | from borax.calendars.lunardate import LunarDate 5 | from borax.calendars.utils import SCalendars, ThreeNineUtils 6 | 7 | 8 | class LastDayTestCase(unittest.TestCase): 9 | def test_last_day(self): 10 | self.assertEqual(date(2019, 3, 31), SCalendars.get_last_day_of_this_month(2019, 3)) 11 | self.assertEqual(date(2019, 2, 28), SCalendars.get_last_day_of_this_month(2019, 2)) 12 | self.assertEqual(date(2020, 2, 29), SCalendars.get_last_day_of_this_month(2020, 2)) 13 | 14 | def test_fist_day_of_week(self): 15 | self.assertEqual(date(2020, 2, 24), SCalendars.get_fist_day_of_year_week(2020, 8)) 16 | self.assertEqual(date(2020, 1, 6), SCalendars.get_fist_day_of_year_week(2020, 1)) 17 | 18 | 19 | class ThreeNineTestCase(unittest.TestCase): 20 | def test_get_39label(self): 21 | self.assertEqual('九九第1天', ThreeNineUtils.get_39label(date(2022, 3, 3))) 22 | self.assertEqual('', ThreeNineUtils.get_39label(date(2022, 4, 12))) 23 | self.assertEqual('九九第1天', ThreeNineUtils.get_39label(LunarDate.from_solar(date(2022, 3, 3)))) 24 | self.assertEqual('中伏第1天', ThreeNineUtils.get_39label(date(2021, 7, 21))) 25 | 26 | def test_39label_for_one_day(self): 27 | d = ThreeNineUtils.get_39days(2022)['初伏'] 28 | self.assertEqual(date(2022, 7, 16), d) 29 | self.assertEqual('庚', LunarDate.from_solar(d).gz_day[0]) 30 | self.assertEqual('初伏第10天', ThreeNineUtils.get_39label(d + timedelta(days=9))) 31 | self.assertEqual('中伏第1天', ThreeNineUtils.get_39label(d + timedelta(days=10))) 32 | self.assertEqual('', ThreeNineUtils.get_39label(date(2021, 9, 30))) 33 | -------------------------------------------------------------------------------- /tests/test_chinese_number.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.numbers import ChineseNumbers 4 | 5 | 6 | class ChineseNumberTestCase(unittest.TestCase): 7 | def test_chinese_number(self): 8 | self.assertEqual('一亿', ChineseNumbers.to_chinese_number(100000000)) 9 | self.assertEqual('一千零四', ChineseNumbers.to_chinese_number(1004)) 10 | self.assertEqual('二千零二十', ChineseNumbers.to_chinese_number(2020)) 11 | self.assertEqual('十一', ChineseNumbers.measure_number(11)) 12 | self.assertEqual('二十八', ChineseNumbers.measure_number('28')) 13 | with self.assertRaises(ValueError): 14 | ChineseNumbers.to_chinese_number(-1) 15 | 16 | def test_chinese_number_lower_order(self): 17 | self.assertEqual('二百〇四', ChineseNumbers.order_number(204)) 18 | self.assertEqual('一千〇五十六', ChineseNumbers.order_number(1056)) 19 | 20 | def test_chinese_number_upper_measure(self): 21 | self.assertEqual('贰佰零肆', ChineseNumbers.measure_number(204, True)) 22 | self.assertEqual('贰佰零肆', ChineseNumbers.to_chinese_number(204, upper=True)) 23 | 24 | def test_chinese_number_upper_order(self): 25 | self.assertEqual('贰佰〇肆', ChineseNumbers.order_number(204, upper=True)) 26 | self.assertEqual('贰佰〇肆', ChineseNumbers.to_chinese_number(204, upper=True, order=True)) 27 | -------------------------------------------------------------------------------- /tests/test_choices.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax import choices 4 | 5 | 6 | class Demo1Field(choices.ConstChoices): 7 | A = 1, 'VER' 8 | B = 2, 'STE' 9 | c = 3, 'SSS' 10 | D = 5 11 | _E = 6, 'ES' 12 | F = 8 13 | G = choices.Item(10) 14 | 15 | 16 | class FieldChoiceTestCase(unittest.TestCase): 17 | def test_get_value(self): 18 | self.assertEqual(1, Demo1Field.A) 19 | self.assertEqual(2, Demo1Field.B) 20 | self.assertEqual(5, Demo1Field.D) 21 | 22 | def test_is_valid(self): 23 | self.assertTrue(Demo1Field.is_valid(1)) 24 | self.assertTrue(Demo1Field.is_valid(2)) 25 | self.assertTrue(Demo1Field.is_valid(3)) 26 | self.assertFalse(Demo1Field.is_valid(4)) 27 | self.assertTrue(Demo1Field.is_valid(5)) 28 | self.assertFalse(Demo1Field.is_valid(6)) 29 | 30 | def test_get_display(self): 31 | self.assertEqual('VER', Demo1Field.get_value_display(1)) 32 | self.assertIsNone(Demo1Field.get_value_display(4)) 33 | self.assertEqual('5', Demo1Field.get_value_display(5)) 34 | self.assertEqual('8', Demo1Field.get_value_display(8)) 35 | self.assertEqual('10', Demo1Field.get_value_display(10)) 36 | 37 | 38 | class GenderChoices(choices.ConstChoices): 39 | MALE = choices.Item(1, 'Male') 40 | FEMALE = choices.Item(2, 'Female') 41 | UNKNOWN = choices.Item(3, 'Unknown') 42 | 43 | 44 | class ChoicesItemTestCase(unittest.TestCase): 45 | def test_get_value(self): 46 | self.assertEqual(1, GenderChoices.MALE) 47 | 48 | def test_is_valid(self): 49 | self.assertTrue(GenderChoices.is_valid(1)) 50 | self.assertTrue(GenderChoices.is_valid(2)) 51 | self.assertTrue(GenderChoices.is_valid(3)) 52 | self.assertFalse(GenderChoices.is_valid(4)) 53 | 54 | def test_get_display(self): 55 | self.assertEqual('Male', GenderChoices.get_value_display(1)) 56 | self.assertEqual('Female', GenderChoices.get_value_display(2)) 57 | self.assertIsNone(Demo1Field.get_value_display(4)) 58 | 59 | def test_get_names_and_values_and_labels(self): 60 | self.assertTupleEqual(('MALE', 'FEMALE', 'UNKNOWN'), GenderChoices.names) 61 | self.assertTupleEqual((1, 2, 3), GenderChoices.values) 62 | self.assertTupleEqual(('Male', 'Female', 'Unknown'), GenderChoices.displays) 63 | self.assertTupleEqual(('Male', 'Female', 'Unknown'), GenderChoices.labels) 64 | 65 | def test_choices(self): 66 | self.assertListEqual([(1, 'Male'), (2, 'Female'), (3, 'Unknown')], GenderChoices.choices) 67 | 68 | 69 | class OffsetChoices(choices.ConstChoices): 70 | up = choices.Item((0, -1), 'Up') 71 | down = choices.Item((0, 1), 'Down') 72 | left = choices.Item((-1, 0), 'Left') 73 | right = choices.Item((0, 1), 'Right') 74 | 75 | 76 | class OffsetChoicesTestCase(unittest.TestCase): 77 | def test_get_value(self): 78 | self.assertEqual((0, -1), OffsetChoices.up) 79 | 80 | def test_is_valid(self): 81 | self.assertTrue(OffsetChoices.is_valid((0, 1))) 82 | self.assertTrue(OffsetChoices.is_valid((-1, 0))) 83 | self.assertFalse(OffsetChoices.is_valid((0, 0))) 84 | self.assertFalse(OffsetChoices.is_valid(0)) 85 | 86 | def test_get_display(self): 87 | self.assertEqual('Up', OffsetChoices.get_value_display((0, -1))) 88 | self.assertEqual('Left', OffsetChoices.get_value_display((-1, 0))) 89 | self.assertIsNone(OffsetChoices.get_value_display((0, 0))) 90 | 91 | 92 | # --------- Class Inheritance ---------------------- 93 | 94 | 95 | class VerticalChoices(choices.ConstChoices): 96 | S = choices.Item('S', 'south') 97 | N = choices.Item('N', 'north') 98 | 99 | 100 | class DirectionChoices(VerticalChoices): 101 | E = choices.Item('E', 'east') 102 | W = choices.Item('W', 'west') 103 | 104 | 105 | class OrderTestChoices(VerticalChoices): 106 | N = choices.Item('n', 'North', order=-1) 107 | 108 | 109 | class DirectionChoicesTestCase(unittest.TestCase): 110 | def test_child_class(self): 111 | self.assertEqual(2, len(VerticalChoices.choices)) 112 | self.assertEqual(2, len(VerticalChoices)) 113 | 114 | self.assertEqual(4, len(DirectionChoices.choices)) 115 | self.assertEqual(4, len(DirectionChoices)) 116 | 117 | expected = [('S', 'south'), ('N', 'north'), ('E', 'east'), ('W', 'west')] 118 | self.assertListEqual(expected, DirectionChoices.choices) 119 | self.assertListEqual(expected, list(DirectionChoices)) 120 | 121 | def test_item_overwrite(self): 122 | self.assertEqual(2, len(OrderTestChoices.choices)) 123 | self.assertEqual('n', OrderTestChoices.N) 124 | self.assertEqual('N', VerticalChoices.choices[1][0]) 125 | self.assertEqual('n', OrderTestChoices.choices[0][0]) 126 | -------------------------------------------------------------------------------- /tests/test_daily_counter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.counters.daily import DailyCounter 4 | 5 | 6 | class DailyCounterTestCase(unittest.TestCase): 7 | def test_init_instance(self): 8 | dc = DailyCounter(2015, 9) 9 | self.assertEqual(30, dc.days) 10 | self.assertEqual(2015, dc.year) 11 | self.assertEqual(9, dc.month) 12 | self.assertRaises(ValueError, dc.get_day_counter, 31) 13 | 14 | def test_init_instance_with_invalid_raw(self): 15 | self.assertRaises(ValueError, DailyCounter, 2015, 9, 'a,b,c') 16 | self.assertRaises(ValueError, DailyCounter, 2015, 9, '0,0,0') 17 | 18 | def test_increase(self): 19 | dc = DailyCounter(2015, 8) 20 | dc.increase(4, 2) 21 | self.assertTrue(2 == dc.get_day_counter(4)) 22 | -------------------------------------------------------------------------------- /tests/test_date_pickle.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pickle 3 | from io import BytesIO 4 | 5 | from datetime import date 6 | 7 | from borax.calendars.lunardate import LunarDate 8 | from borax.calendars.festivals2 import WrappedDate 9 | 10 | 11 | class WrappedDateBasicTestCase(unittest.TestCase): 12 | def test_wrapped_date(self): 13 | ld = LunarDate.today() 14 | wd = WrappedDate(ld) 15 | self.assertEqual(ld, wd.lunar) 16 | with self.assertRaises(AttributeError): 17 | wd.lunar = LunarDate(2024, 1, 1) 18 | 19 | 20 | class DatePickleTestCase(unittest.TestCase): 21 | 22 | def test_lunardate_pickle(self): 23 | ld1 = LunarDate.today() 24 | fp = BytesIO() 25 | 26 | pickle.dump(ld1, fp) 27 | 28 | fp.seek(0) 29 | e_ld = pickle.load(fp) 30 | self.assertEqual(ld1, e_ld) 31 | 32 | def test_wrapped_date_pickle(self): 33 | wd_list = [WrappedDate(date.today()), WrappedDate(LunarDate.today())] 34 | for wd in wd_list: 35 | with self.subTest(wd=wd): 36 | fp = BytesIO() 37 | pickle.dump(wd, fp) 38 | fp.seek(0) 39 | new_wd = pickle.load(fp) 40 | self.assertEqual(wd.solar, new_wd.solar) 41 | self.assertEqual(wd.lunar, new_wd.lunar) 42 | -------------------------------------------------------------------------------- /tests/test_dictionary.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.structures.dictionary import AttributeDict 4 | 5 | 6 | class AttributeDictTestCase(unittest.TestCase): 7 | def test_access(self): 8 | m = AttributeDict({'foo': 'bar'}) 9 | self.assertEqual('bar', m.foo) 10 | m.foo = 'not bar' 11 | self.assertEqual('not bar', m['foo']) 12 | -------------------------------------------------------------------------------- /tests/test_festival_library.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import date 3 | from io import StringIO 4 | from unittest.mock import MagicMock, patch 5 | 6 | from borax.calendars.festivals2 import (LunarFestival, TermFestival, FestivalLibrary, FestivalSchema, 7 | FestivalDatasetNotExist) 8 | 9 | 10 | class FestivalLibraryTestCase(unittest.TestCase): 11 | def test_library(self): 12 | fl = FestivalLibrary.load_builtin() 13 | self.assertEqual(33, len(fl)) 14 | 15 | spring_festival = fl.get_festival('春节') 16 | self.assertTrue(isinstance(spring_festival, LunarFestival)) 17 | 18 | names = fl.get_festival_names(date_obj=date(2021, 10, 1)) 19 | self.assertListEqual(['国庆节'], names) 20 | 21 | gd_days = [] 22 | for nday, gd_list in fl.iter_festival_countdown(date_obj=date(2021, 1, 1), countdown=31): 23 | gd_days.extend(gd_list) 24 | 25 | self.assertIn('元旦', [g.name for g in gd_days]) 26 | 27 | def test_new_load(self): 28 | fl = FestivalLibrary.load('basic') 29 | self.assertEqual(33, len(fl)) 30 | with self.assertRaises(FestivalDatasetNotExist): 31 | FestivalLibrary.load('not-found') 32 | 33 | def test_list_days(self): 34 | fl = FestivalLibrary.load_builtin() 35 | fes_list = [] 36 | for _, wd, festival in fl.list_days_in_countdown(countdown=365): 37 | fes_list.append(festival.name) 38 | self.assertIn('元旦', fes_list) 39 | 40 | festival_names = [items[1].name for items in fl.list_days(date(2022, 1, 1), date(2022, 12, 31))] 41 | self.assertIn('元旦', festival_names) 42 | 43 | def test_builtin_libraries(self): 44 | fl = FestivalLibrary.load_builtin('empty') 45 | self.assertEqual(0, len(fl)) 46 | fl1 = FestivalLibrary.load_builtin('basic1') 47 | self.assertEqual(55, len(fl1)) 48 | 49 | 50 | class FestivalLibraryUniqueTestCase(unittest.TestCase): 51 | def test_unique(self): 52 | fl = FestivalLibrary() 53 | ft1 = TermFestival(name='冬至') 54 | fl.append(ft1) 55 | self.assertEqual(1, len(fl.get_code_set())) 56 | ft2 = TermFestival(index=23) 57 | fl.extend_unique([ft2]) 58 | self.assertEqual(1, len(fl)) 59 | ft3 = TermFestival(name='小寒') 60 | fl.extend_unique([ft3]) 61 | self.assertEqual(2, len(fl)) 62 | fl.extend_unique(['205026', '89005']) 63 | self.assertEqual(3, len(fl)) 64 | 65 | def test_unique_for_basic_library(self): 66 | fl = FestivalLibrary.load_builtin('basic') 67 | total_1 = len(fl) 68 | fl.extend_term_festivals() 69 | total_2 = len(fl) 70 | self.assertEqual(22, total_2 - total_1) 71 | 72 | def test_edit(self): 73 | fl = FestivalLibrary() 74 | fl.load_term_festivals() 75 | self.assertEqual(24, len(fl)) 76 | 77 | def test_save(self): 78 | fileobj = StringIO() 79 | fl = FestivalLibrary.load_builtin() 80 | fl.to_csv(fileobj) 81 | 82 | mock = MagicMock() 83 | mock.write.return_value = None 84 | with patch('builtins.open', mock): 85 | fl.to_csv('xxx.csv') 86 | 87 | def test_filter(self): 88 | fl = FestivalLibrary.load_builtin('ext1') 89 | basic_fl = fl.filter(catalogs='basic') 90 | basic_fl2 = fl.filter_(catalog='basic') 91 | self.assertTrue(len(basic_fl) > 0) 92 | self.assertTrue(len(basic_fl) == len(basic_fl2)) 93 | 94 | 95 | class FestivalLibraryCalendarTestCase(unittest.TestCase): 96 | def test_calendar(self): 97 | fl = FestivalLibrary.load_builtin() 98 | days = fl.monthdaycalendar(2022, 1) 99 | self.assertEqual(6, len(days)) 100 | fl1 = FestivalLibrary(fl) 101 | self.assertTrue(isinstance(fl1, FestivalLibrary)) 102 | self.assertTrue(len(fl) == len(fl1)) 103 | 104 | 105 | class FestivalLibraryCURDTestCase(unittest.TestCase): 106 | 107 | def test_filter_inplace(self): 108 | fl = FestivalLibrary.load_builtin() 109 | total = len(fl) 110 | self.assertTrue(total > 1) 111 | name1 = fl[0].name 112 | fl.filter_inplace(name=name1) 113 | self.assertEqual(1, len(fl)) 114 | 115 | def test_exclude_inplace(self): 116 | fl = FestivalLibrary.load_builtin() 117 | total = len(fl) 118 | name1 = fl[0].name 119 | name2 = fl[1].name 120 | fl.exclude_inplace(name=name1) 121 | self.assertEqual(total - 1, len(fl)) 122 | self.assertEqual(name2, fl[0].name) 123 | 124 | def test_schema(self): 125 | fl = FestivalLibrary.load_builtin() 126 | total0 = len(fl) 127 | schema_totals = {} 128 | for festival in fl: 129 | if festival.schema not in schema_totals: 130 | schema_totals[festival.schema] = 1 131 | else: 132 | schema_totals[festival.schema] += 1 133 | fl.exclude_inplace(schema=FestivalSchema.SOLAR) 134 | self.assertEqual(total0 - schema_totals[FestivalSchema.SOLAR], len(fl)) 135 | fl.exclude_inplace(schema__in=[FestivalSchema.WEEK, FestivalSchema.TERM]) 136 | self.assertEqual(schema_totals[FestivalSchema.LUNAR], len(fl)) 137 | 138 | def test_catalog(self): 139 | fl = FestivalLibrary.load_builtin('ext1') 140 | fl.filter_inplace(catalog='basic') 141 | self.assertTrue(len(fl) > 0) 142 | 143 | def test_filter_exclude_copy(self): 144 | fl = FestivalLibrary.load_builtin() 145 | fl1 = fl.filter_(schema__in=[FestivalSchema.SOLAR, FestivalSchema.LUNAR]) 146 | fl2 = fl.filter_(schema__in=[FestivalSchema.WEEK, FestivalSchema.TERM]) 147 | fl3 = fl.exclude_(schema__in=[FestivalSchema.WEEK, FestivalSchema.TERM]) 148 | self.assertEqual(len(fl), len(fl1) + len(fl2)) 149 | self.assertEqual(len(fl3), len(fl1)) 150 | 151 | def test_festivals_functional_program(self): 152 | fl = FestivalLibrary.load_builtin() 153 | fl2 = FestivalLibrary(filter(lambda f: f.schema == FestivalSchema.SOLAR, fl)) 154 | self.assertTrue(isinstance(fl2, FestivalLibrary)) 155 | -------------------------------------------------------------------------------- /tests/test_festivals2_serialize.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | import unittest 3 | from datetime import date 4 | 5 | from borax.calendars.festivals2 import SolarFestival, LunarFestival, WeekFestival, TermFestival, decode, \ 6 | decode_festival, WrappedDate, FestivalError 7 | from borax.calendars.lunardate import LunarDate 8 | 9 | 10 | class FestivalEncodeTestCase(unittest.TestCase): 11 | def test_festival_encode(self): 12 | new_year = SolarFestival(month=1, day=1) 13 | self.assertEqual('001010', new_year.encode()) 14 | teacher_day = SolarFestival(month=9, day=10) 15 | self.assertEqual('009100', teacher_day.encode()) 16 | lf2 = LunarFestival(month=5, day=5) 17 | self.assertEqual('105050', lf2.encode()) 18 | month_day = WeekFestival(month=5, index=2, week=calendar.SUNDAY) 19 | self.assertEqual('205026', month_day.encode()) 20 | tt = TermFestival(name='冬至') 21 | self.assertEqual('400230', tt.encode()) 22 | sf2 = SolarFestival(day=-2) 23 | self.assertEqual('00002A', sf2.encode()) 24 | lf3 = LunarFestival(day=-2) 25 | self.assertEqual('10002A', lf3.encode()) 26 | 27 | def test_old_lunar(self): 28 | lf = decode_festival('312011') 29 | lf2 = LunarFestival(month=12, day=-1) 30 | self.assertEqual(lf.encode(), lf2.encode()) 31 | lf = decode_festival('312010') 32 | lf2 = LunarFestival(month=12, day=1) 33 | self.assertEqual(lf.encode(), lf2.encode()) 34 | 35 | 36 | class FestivalDecodeTestCase(unittest.TestCase): 37 | def test_festival_decode(self): 38 | raw = '001010' 39 | f = decode_festival(raw) 40 | self.assertEqual(raw, f.encode()) 41 | raw2 = '0202001010' 42 | f2 = decode_festival(raw2) 43 | self.assertEqual(raw, f2.encode()) 44 | 45 | def test_fail_decode(self): 46 | with self.assertRaises(ValueError): 47 | decode_festival('XEDE') 48 | with self.assertRaises(ValueError): 49 | decode_festival('123456789') 50 | with self.assertRaises(ValueError): 51 | decode_festival('654321') 52 | 53 | def test_all(self): 54 | all_codes = [ 55 | '001010', '009100', '105050', '205026', '400230', '00002A', '00112A', '00003C', '201116', 56 | '411102', '421102', '43110B', '44110B'] 57 | for raw in all_codes: 58 | with self.subTest(raw=raw): 59 | f = decode_festival(raw) 60 | self.assertEqual(raw, f.encode()) 61 | self.assertEqual(raw, decode(raw).encode()) 62 | 63 | 64 | class DateEncoderTestCase(unittest.TestCase): 65 | def test_date_encode(self): 66 | ld = LunarDate(2021, 8, 15) 67 | self.assertEqual('1202108150', WrappedDate(ld).encode()) 68 | self.assertEqual('0202109210', WrappedDate(ld.to_solar_date()).encode()) 69 | 70 | ld = LunarDate(2020, 4, 3, 1) 71 | self.assertEqual('1202004031', WrappedDate(ld).encode()) 72 | 73 | def test_date_decode(self): 74 | wd = WrappedDate.decode('0202109210') 75 | self.assertEqual(date(2021, 9, 21), wd.solar) 76 | 77 | wd1 = WrappedDate.decode('1202004031') 78 | self.assertEqual(LunarDate(2020, 4, 3, 1), wd1.lunar) 79 | 80 | wd2 = WrappedDate.decode('1202004030') 81 | self.assertEqual(LunarDate(2020, 4, 3, 0), wd2.lunar) 82 | 83 | def test_exception(self): 84 | with self.assertRaises(FestivalError): 85 | WrappedDate.decode('4202100230') 86 | -------------------------------------------------------------------------------- /tests/test_festivals_datasets.csv: -------------------------------------------------------------------------------- 1 | 001010,元旦 2 | 002140,情人节 3 | 003080,妇女节 4 | 003120,植树节 5 | 004010,愚人节 6 | 005010,劳动节 7 | 005040,青年节 8 | 005120,护士节 9 | 006010,儿童节 10 | 008010,建军节 11 | 009100,教师节 12 | 010010,国庆节 13 | 012240,平安夜 14 | 012250,圣诞节 15 | 101010,春节 16 | 101150,元宵节 17 | 102020,龙头节 18 | 103030,上巳节 19 | 105050,端午节 20 | 107070,七夕 21 | 107150,中元节 22 | 108150,中秋节 23 | 109090,重阳节 24 | 112080,腊八节 25 | 10001A,除夕 26 | 205026,母亲节 27 | 206036,父亲节 28 | 211043,感恩节 29 | 400060,清明 30 | 400230,冬至 31 | 413116,初伏 32 | 414116,中伏 33 | 411146,末伏 -------------------------------------------------------------------------------- /tests/test_fetch.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.datasets.fetch import fetch, fetch_single, ifetch_multiple, fetch_as_dict, bget 4 | 5 | DICT_LIST_DATA = [ 6 | {'id': 282, 'name': 'Alice', 'age': 30, 'sex': 'female'}, 7 | {'id': 217, 'name': 'Bob', 'age': 56}, 8 | {'id': 328, 'name': 'Charlie', 'age': 56, 'sex': 'male'}, 9 | ] 10 | 11 | 12 | class Point: 13 | def __init__(self, x, y): 14 | self.x = x 15 | self.y = y 16 | 17 | 18 | class GetterTestCase(unittest.TestCase): 19 | def test_default_getter(self): 20 | p = Point(1, 2) 21 | self.assertEqual(1, bget(p, 'x')) 22 | p2 = [2, 4] 23 | self.assertEqual(4, bget(p2, 1)) 24 | p3 = {'x': 5, 'y': 10} 25 | self.assertEqual(5, bget(p3, 'x')) 26 | 27 | 28 | class FetchTestCase(unittest.TestCase): 29 | def test_fetch_single(self): 30 | names = fetch_single(DICT_LIST_DATA, 'name') 31 | self.assertListEqual(names, ['Alice', 'Bob', 'Charlie']) 32 | sexs = fetch_single(DICT_LIST_DATA, 'sex', default='male') 33 | self.assertListEqual(sexs, ['female', 'male', 'male']) 34 | 35 | def test_ifetch_multiple(self): 36 | names, ages = map(list, ifetch_multiple(DICT_LIST_DATA, 'name', 'age')) 37 | self.assertListEqual(names, ['Alice', 'Bob', 'Charlie']) 38 | self.assertListEqual(ages, [30, 56, 56]) 39 | 40 | def test_fetch(self): 41 | names = fetch(DICT_LIST_DATA, 'name') 42 | self.assertListEqual(names, ['Alice', 'Bob', 'Charlie']) 43 | 44 | sexs = fetch(DICT_LIST_DATA, 'sex', default='male') 45 | self.assertListEqual(sexs, ['female', 'male', 'male']) 46 | 47 | names, ages = fetch(DICT_LIST_DATA, 'name', 'age') 48 | self.assertListEqual(names, ['Alice', 'Bob', 'Charlie']) 49 | self.assertListEqual(ages, [30, 56, 56]) 50 | 51 | names, ages, sexs = fetch(DICT_LIST_DATA, 'name', 'age', 'sex', defaults={'sex': 'male'}) 52 | self.assertListEqual(names, ['Alice', 'Bob', 'Charlie']) 53 | self.assertListEqual(ages, [30, 56, 56]) 54 | self.assertListEqual(sexs, ['female', 'male', 'male']) 55 | 56 | 57 | class MockItem: 58 | def __init__(self, x, y, z): 59 | self._data = {'x': x, 'y': y, 'z': z} 60 | 61 | def get(self, key): 62 | return self._data.get(key) 63 | 64 | 65 | class FetchCustomGetterTestCase(unittest.TestCase): 66 | def test_custom_getter(self): 67 | data_list = [MockItem(1, 2, 3), MockItem(4, 5, 6), MockItem(7, 8, 9)] 68 | xs, ys, zs = fetch(data_list, 'x', 'y', 'z', getter=lambda item, key: item.get(key)) 69 | self.assertListEqual([1, 4, 7], xs) 70 | 71 | def test_with_dict(self): 72 | """ 73 | Use dict.get(key) to pick item. 74 | """ 75 | names, ages = fetch(DICT_LIST_DATA, 'name', 'age', getter=lambda item, key: item.get(key)) 76 | self.assertListEqual(names, ['Alice', 'Bob', 'Charlie']) 77 | self.assertListEqual(ages, [30, 56, 56]) 78 | 79 | 80 | class FetchAsDictTestCase(unittest.TestCase): 81 | def test_fetch_as_dict(self): 82 | objects = [ 83 | {'id': 282, 'name': 'Alice', 'age': 30}, 84 | {'id': 217, 'name': 'Bob', 'age': 56}, 85 | {'id': 328, 'name': 'Charlie', 'age': 56}, 86 | ] 87 | data_dict = fetch_as_dict(objects, 'id', 'name') 88 | self.assertDictEqual({282: 'Alice', 217: 'Bob', 328: 'Charlie'}, data_dict) 89 | 90 | 91 | class FetchFromTuplesTestCase(unittest.TestCase): 92 | def test_fetch_from_tuples(self): 93 | data = [ 94 | [1, 2, 3, 4, 5, 6, 7, 8, 9], 95 | [11, 12, 13, 14, 15, 16, 17, 18, 19], 96 | [21, 22, 23, 24, 25, 26, 27, 28, 29] 97 | ] 98 | ones, threes = fetch(data, 0, 2) 99 | self.assertListEqual(ones, [1, 11, 21]) 100 | self.assertListEqual(threes, [3, 13, 23]) 101 | -------------------------------------------------------------------------------- /tests/test_finance.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | import unittest 3 | 4 | from borax.numbers import FinanceNumbers 5 | 6 | decimal.getcontext().prec = 2 7 | 8 | 9 | class CapitalNumber(unittest.TestCase): 10 | def test_amount_capital(self): 11 | self.assertEqual('壹亿元整', FinanceNumbers.to_capital_str(100000000)) 12 | self.assertEqual('肆佰伍拾柒万捌仟肆佰肆拾贰元贰角叁分', FinanceNumbers.to_capital_str(4578442.23)) 13 | self.assertEqual('贰佰叁拾肆元整', FinanceNumbers.to_capital_str(234)) 14 | self.assertEqual('捌拾元零角贰分', FinanceNumbers.to_capital_str(80.02)) 15 | 16 | def test_decimal(self): 17 | self.assertEqual('肆元伍角零分', FinanceNumbers.to_capital_str(decimal.Decimal(4.50))) 18 | self.assertEqual('壹拾万柒仟元伍角叁分', FinanceNumbers.to_capital_str(decimal.Decimal('107000.53'))) 19 | self.assertEqual('壹拾万柒仟元伍角叁分', FinanceNumbers.to_capital_str('107000.53')) 20 | 21 | def test_valid_range(self): 22 | with self.assertRaises(ValueError): 23 | FinanceNumbers.to_capital_str(332342342341234) 24 | with self.assertRaises(ValueError): 25 | FinanceNumbers.to_capital_str(1000000000000) 26 | self.assertIsNotNone(FinanceNumbers.to_capital_str(999999999999)) 27 | self.assertIsNotNone(FinanceNumbers.to_capital_str(999999999999.99)) 28 | with self.assertRaises(ValueError): 29 | FinanceNumbers.to_capital_str('1000000000000') 30 | -------------------------------------------------------------------------------- /tests/test_htmls.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.htmls import HTMLString, html_tag 4 | 5 | 6 | class HtmlStringTestCase(unittest.TestCase): 7 | def test_html_string(self): 8 | s = HTMLString('
Hello
') 9 | s2 = HTMLString(s) 10 | self.assertEqual(s.__html__(), s2.__html__()) 11 | 12 | 13 | class HtmlTagTest(unittest.TestCase): 14 | def test_html_tags(self): 15 | html = html_tag('img', id_='idDemoImg', src='/demo.png') 16 | self.assertEqual(html, '') 17 | html = html_tag('div', content='Test', id_='idDemo', data_my_attr='23') 18 | self.assertEqual(html, '
Test
') 19 | 20 | self.assertEqual('test', HTMLString.escape('test')) 21 | 22 | def test_fixed_html(self): 23 | # Add v3.5.2 24 | self.assertEqual('
', html_tag('div', id_='demo')) 25 | self.assertEqual('
', 26 | html_tag('div', id_='demo', style={'width': '2px'})) 27 | self.assertEqual('
', html_tag('div', id_='demo', class_=['a1', 'a2'])) 28 | 29 | def test_css_attr(self): 30 | self.assertEqual('
', html_tag('div', class_=['one', 'two'])) 31 | self.assertEqual('
', html_tag('div', class_='one two')) 32 | 33 | def test_bool_attr(self): 34 | self.assertEqual('
Demo
', html_tag('div', id_='sk', content='Demo', checked=True)) 35 | self.assertEqual('
Demo
', html_tag('div', id_='sk', content='Demo', checked=False)) 36 | 37 | def test_style_none(self): 38 | html_str = html_tag('div', style={'width': '200px', 'height': None}) 39 | self.assertNotIn('height', html_str) 40 | html_str = html_tag('div', style_width='200px') 41 | self.assertIn('width:200px', html_str) 42 | html_str = html_tag('div', style_width='200px', style_height='800px', style={'width': '500px'}) 43 | self.assertIn('width:200px', html_str) 44 | self.assertIn('height:800px', html_str) 45 | html_str = html_tag('div', width='200px') 46 | self.assertIn('width="200px"', html_str) 47 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | 4 | from borax.serialize import cjson 5 | 6 | 7 | class Point: 8 | def __init__(self, x, y): 9 | self.x = x 10 | self.y = y 11 | 12 | def __json__(self): 13 | return [self.x, self.y] 14 | 15 | 16 | class EPoint: 17 | def __init__(self, x, y): 18 | self.x = x 19 | self.y = y 20 | 21 | 22 | @cjson.encoder.register(EPoint) 23 | def encode_epoint(o): 24 | return [o.x, o.y] 25 | 26 | 27 | class CJsonTestCase(unittest.TestCase): 28 | def test_dumps(self): 29 | obj = {'point': Point(1, 2), 'a': 4} 30 | output = cjson.dumps(obj) 31 | expected = ['{"point": [1, 2], "a": 4}', '{"a": 4, "point": [1, 2]}'] 32 | self.assertIn(output, expected) 33 | 34 | def test_custom_encoder(self): 35 | obj = {'point': Point(1, 2)} 36 | output = json.dumps(obj, default=cjson.encoder) 37 | self.assertEqual('{"point": [1, 2]}', output) 38 | 39 | def test_singledispatch(self): 40 | obj = {'point': EPoint(1, 2)} 41 | output = cjson.dumps(obj) 42 | self.assertEqual('{"point": [1, 2]}', output) 43 | 44 | def test_mixed_encoder(self): 45 | class Pt: 46 | def __init__(self, x, y): 47 | self.x = x 48 | self.y = y 49 | 50 | def __json__(self): 51 | return [self.x, self.y] 52 | 53 | @cjson.encoder.register(Pt) 54 | def encode_pt(p): 55 | return {'x': p.x, 'y': p.y} 56 | 57 | obj = {'point': Pt(1, 2)} 58 | output = cjson.dumps(obj) 59 | self.assertEqual('{"point": {"x": 1, "y": 2}}', output) 60 | 61 | cjson.encoder.register(Pt, cjson.encode_object) 62 | 63 | obj = {'point': Pt(1, 2)} 64 | output = cjson.dumps(obj) 65 | self.assertEqual('{"point": [1, 2]}', output) 66 | 67 | def test_encoder_class(self): 68 | obj = {'point': Point(1, 2)} 69 | output = json.dumps(obj, cls=cjson.CJSONEncoder) 70 | self.assertEqual('{"point": [1, 2]}', output) 71 | -------------------------------------------------------------------------------- /tests/test_lazy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.patterns.lazy import LazyObject 4 | 5 | 6 | class MockPoint(object): 7 | def __init__(self, x, y): 8 | self.x = x 9 | self.y = y 10 | 11 | 12 | def create_point(): 13 | return MockPoint(1, 2) 14 | 15 | 16 | def create_point_with_kwargs(x, y): 17 | return MockPoint(x, y) 18 | 19 | 20 | class LazyObjectTestCase(unittest.TestCase): 21 | def test_lazy_object(self): 22 | p = LazyObject(create_point) 23 | self.assertTrue(isinstance(p, LazyObject)) 24 | p.x = 3 25 | self.assertTrue(isinstance(p, MockPoint)) 26 | self.assertEqual(2, p.y) 27 | 28 | def test_with_kwargs(self): 29 | p = LazyObject(MockPoint, kwargs={'x': 1, 'y': 2}) 30 | self.assertTrue(isinstance(p, LazyObject)) 31 | self.assertEqual(1, p.x) 32 | self.assertTrue(isinstance(p, MockPoint)) 33 | 34 | def test_delete_attr(self): 35 | p = LazyObject(MockPoint, kwargs={'x': 1, 'y': 2}) 36 | delattr(p, 'x') 37 | with self.assertRaises(AttributeError): 38 | print(p.x) 39 | -------------------------------------------------------------------------------- /tests/test_lunar_benchmark.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.calendars.lunardate import ( 4 | LunarDate, MAX_OFFSET, 5 | YEAR_DAYS, MIN_SOLAR_DATE, 6 | MAX_SOLAR_DATE 7 | ) 8 | 9 | 10 | class BenchmarkTestCase(unittest.TestCase): 11 | def test_edge_dates(self): 12 | # Max date 13 | self.assertEqual(MAX_OFFSET, LunarDate.max.offset) 14 | self.assertEqual(MAX_OFFSET, (LunarDate.max - LunarDate.min).days) 15 | self.assertEqual(MAX_OFFSET + 1, sum(YEAR_DAYS)) 16 | 17 | self.assertEqual(0, (LunarDate.min - MIN_SOLAR_DATE).days) 18 | self.assertEqual(0, (LunarDate.max - MAX_SOLAR_DATE).days) 19 | 20 | sd2100_ld = LunarDate.from_solar_date(2100, 12, 31) 21 | self.assertEqual('庚申年戊子月丁未日', sd2100_ld.gz_str()) 22 | sd2101_ld = LunarDate.from_solar_date(2101, 1, 28) 23 | self.assertEqual('庚申年己丑月乙亥日', sd2101_ld.gz_str()) 24 | -------------------------------------------------------------------------------- /tests/test_lunar_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.calendars.data_parser import strptime 4 | from borax.calendars.lunardate import LunarDate 5 | 6 | 7 | class ParserTestCase(unittest.TestCase): 8 | def test_parse(self): 9 | self.assertEqual(LunarDate(2022, 4, 1), strptime('2022041', '%y%l%m%d')) 10 | self.assertEqual(LunarDate(2022, 4, 1), strptime('二〇二二041', '%Y%l%m%d')) 11 | self.assertEqual(LunarDate(2020, 4, 23, 1), strptime('20201423', '%y%l%m%d')) 12 | self.assertEqual(LunarDate(2022, 4, 23, 0), strptime('二〇二二年四月廿三', '%Y年%M月%D')) 13 | self.assertEqual(LunarDate(2022, 4, 23, 0), strptime('二〇二二年四月廿三', '%Y年%L%M月%D')) 14 | self.assertEqual(LunarDate(2020, 4, 23, 1), strptime('二〇二〇年闰四月廿三', '%Y年%L%M月%D')) 15 | self.assertEqual(LunarDate(2020, 4, 20, 1), strptime('二〇二〇年闰四月二十', '%Y年%L%M月%D')) 16 | 17 | def test_parse_with_cn(self): 18 | self.assertEqual(LunarDate(2020, 4, 23, 0), strptime('二〇二〇年四月廿三', '%C')) 19 | self.assertEqual(LunarDate(2020, 4, 23, 1), strptime('二〇二〇年闰四月廿三', '%C')) 20 | 21 | self.assertEqual(LunarDate(2020, 4, 23, 1), LunarDate.strptime('二〇二〇年闰四月廿三', '%C')) 22 | 23 | def test_fail_parse(self): 24 | with self.assertRaises(ValueError): 25 | LunarDate.strptime('二〇二〇年闰四月二十 2021', '%Y年%L%M月%D %y') 26 | with self.assertRaises(ValueError): 27 | LunarDate.strptime('二〇二〇年闰四月廿十', '%Y年%L%M月%D') 28 | with self.assertRaises(ValueError): 29 | LunarDate.strptime('二〇二〇年闰四月二十00', '%Y年%L%M月%D') 30 | -------------------------------------------------------------------------------- /tests/test_lunar_sqlite3_feature.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import unittest 3 | 4 | from borax.calendars.lunardate import LunarDate 5 | from borax.calendars.festivals2 import encode, decode, WrappedDate 6 | 7 | sqlite3.register_adapter(WrappedDate, encode) 8 | sqlite3.register_converter("WrappedDate", decode) 9 | 10 | 11 | class Sqlite3CustomFieldTestCase(unittest.TestCase): 12 | def test_custom_field(self): 13 | con = sqlite3.connect(":memory:", detect_types=sqlite3.PARSE_DECLTYPES) 14 | cur = con.cursor() 15 | cur.execute('CREATE TABLE member (pid INT AUTO_INCREMENT PRIMARY KEY,birthday WrappedDate);') 16 | ld = LunarDate(2018, 5, 3) 17 | cur.execute("INSERT INTO member(birthday) VALUES (?)", (WrappedDate(ld),)) 18 | cur.execute("SELECT pid, birthday FROM member;") 19 | my_birthday = cur.fetchone()[1].lunar 20 | cur.close() 21 | con.close() 22 | self.assertEqual(LunarDate, type(my_birthday)) 23 | self.assertEqual(2018, my_birthday.year) 24 | self.assertEqual(5, my_birthday.month) 25 | self.assertEqual(3, my_birthday.day) 26 | self.assertEqual(0, my_birthday.leap) 27 | -------------------------------------------------------------------------------- /tests/test_percentage.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.structures.percentage import Percentage 4 | 5 | 6 | class PercentTestCase(unittest.TestCase): 7 | def test_basic_use(self): 8 | p = Percentage(total=100) 9 | p.increase(34) 10 | 11 | self.assertEqual(100, p.total) 12 | self.assertEqual(34, p.completed) 13 | self.assertAlmostEqual(0.34, p.percent) 14 | self.assertEqual('34.00%', p.percent_display) 15 | self.assertEqual('34 / 100', p.display) 16 | self.assertDictEqual({ 17 | 'total': 100, 18 | 'completed': 34, 19 | 'percent': 0.34, 20 | 'percent_display': '34.00%', 21 | 'display': '34 / 100' 22 | }, p.as_dict()) 23 | 24 | def test_zero_total(self): 25 | p = Percentage(total=0) 26 | p.increase(35) 27 | p.decrease(1) 28 | 29 | self.assertEqual(0, p.total) 30 | self.assertEqual(34, p.completed) 31 | self.assertAlmostEqual(0, p.percent) 32 | self.assertEqual('-', p.percent_display) 33 | self.assertEqual('34 / 0', p.display) 34 | self.assertDictEqual({ 35 | 'total': 0, 36 | 'completed': 34, 37 | 'percent': 0, 38 | 'percent_display': '-', 39 | 'display': '34 / 0' 40 | }, p.as_dict()) 41 | 42 | def test_custom_places(self): 43 | p = Percentage(total=100, places=4) 44 | p.increase(34) 45 | 46 | self.assertEqual(100, p.total) 47 | self.assertEqual(34, p.completed) 48 | self.assertAlmostEqual(0.3400, p.percent) 49 | self.assertEqual('34.0000%', p.percent_display) 50 | self.assertEqual('34 / 100', p.display) 51 | self.assertDictEqual({ 52 | 'total': 100, 53 | 'completed': 34, 54 | 'percent': 0.3400, 55 | 'percent_display': '34.0000%', 56 | 'display': '34 / 100' 57 | }, p.as_dict()) 58 | -------------------------------------------------------------------------------- /tests/test_proxy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.patterns.proxy import Proxy 4 | 5 | 6 | class MockPoint(object): 7 | def __init__(self, x, y): 8 | self.x = x 9 | self.y = y 10 | 11 | def get_distance(self): 12 | return self.x ** 2 + self.y ** 2 13 | 14 | 15 | class ProxyTestCase(unittest.TestCase): 16 | def test_proxy(self): 17 | proxy = Proxy() 18 | with self.assertRaises(AttributeError): 19 | proxy.get_distance() 20 | proxy.initialize(MockPoint(3, 4)) 21 | self.assertEqual(25, proxy.get_distance()) 22 | -------------------------------------------------------------------------------- /tests/test_runtime_measurer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.devtools import RuntimeMeasurer 4 | 5 | 6 | def long_operate(): 7 | for _ in range(100000): 8 | pass 9 | 10 | 11 | class RuntimeMeasurerTestCase(unittest.TestCase): 12 | def test_one_hit(self): 13 | rm = RuntimeMeasurer() 14 | rm.start('xt') 15 | long_operate() 16 | rm.end('xt') 17 | data = rm.get_measure_result() 18 | self.assertEqual(1, data['xt'].count) 19 | self.assertIsNotNone(data['xt'].avg) 20 | 21 | def test_with(self): 22 | rm = RuntimeMeasurer() 23 | with rm.measure('xt'): 24 | long_operate() 25 | data = rm.get_measure_result() 26 | self.assertEqual(1, data['xt'].count) 27 | self.assertIsNotNone(data['xt'].avg) 28 | 29 | def test_multiple_tags(self): 30 | rm = RuntimeMeasurer() 31 | rm.start('tag1') 32 | long_operate() 33 | rm.end('tag1').start('tag2') 34 | long_operate() 35 | rm.end('tag2') 36 | data = rm.get_measure_result() 37 | self.assertTrue('tag1' in data) 38 | self.assertTrue('tag2' in data) 39 | -------------------------------------------------------------------------------- /tests/test_serial_pool.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.counters.serial_pool import serial_no_generator, LabelFormatOpts, SerialNoPool 4 | 5 | 6 | class SerialValueGeneratorTestCase(unittest.TestCase): 7 | def test_value_generator(self): 8 | self.assertEqual([0, 1, 2], list(serial_no_generator(upper=10))[:3]) 9 | source = [0, 1, 2, 4, 5, 7] 10 | res = list(serial_no_generator(upper=10, values=source))[:2] 11 | self.assertListEqual([8, 9], res) 12 | res = list(serial_no_generator(upper=10, values=source))[:3] 13 | self.assertListEqual([8, 9, 3], res) 14 | res = list(serial_no_generator(upper=10, values=source))[:4] 15 | self.assertListEqual([8, 9, 3, 6], res) 16 | self.assertListEqual([9], list(serial_no_generator(reused=False, values=[7, 8]))[:4]) 17 | 18 | 19 | class LabelFormatOptsTestCase(unittest.TestCase): 20 | def test_invalid_opts(self): 21 | with self.assertRaises(ValueError): 22 | LabelFormatOpts('{no:02x}--{no:03d}') 23 | 24 | def test_basic(self): 25 | opts = LabelFormatOpts('LC{no}') 26 | self.assertEqual(2, opts.label2value('LC02')) 27 | self.assertEqual('LC02', opts.value2label(2)) 28 | 29 | def test_hex_value(self): 30 | opts = LabelFormatOpts('FFFF{no:04X}') 31 | self.assertEqual(57159, opts.label2value('FFFFDF47')) 32 | self.assertEqual('FFFFDF47', opts.value2label(57159)) 33 | 34 | opts2 = LabelFormatOpts('FFFF{no:04x}') 35 | self.assertEqual(57159, opts2.label2value('FFFFdf47')) 36 | self.assertEqual('FFFFdf47', opts2.value2label(57159)) 37 | 38 | opts3 = LabelFormatOpts('FFFF{no}', base=16, digits=4) 39 | self.assertEqual(57159, opts3.label2value('FFFFdf47')) 40 | self.assertEqual('FFFFdf47', opts3.value2label(57159)) 41 | 42 | 43 | class SerialNoPoolTestCase(unittest.TestCase): 44 | def test_success_create(self): 45 | sp = SerialNoPool(lower=3, upper=5) 46 | self.assertEqual(3, sp.lower) 47 | self.assertEqual(5, sp.upper) 48 | 49 | def test_error_create(self): 50 | with self.assertRaises(ValueError): 51 | SerialNoPool(label_fmt='{no:02x}--{no:03d}') 52 | with self.assertRaises(ValueError): 53 | SerialNoPool(lower=-2) 54 | with self.assertRaises(ValueError): 55 | SerialNoPool(upper=-500) 56 | with self.assertRaises(ValueError): 57 | SerialNoPool(label_fmt='{no:02d}', lower=2, upper=1000) 58 | 59 | def test_basic_generate(self): 60 | sp = SerialNoPool() 61 | data = list(sp.get_next_generator())[:3] 62 | self.assertEqual(3, len(data)) 63 | self.assertEqual(2, data[2].value) 64 | with self.assertRaises(TypeError): 65 | sp.generate(13) 66 | 67 | def test_custom_label_fmt(self): 68 | sp = SerialNoPool(label_fmt='X{no:01x}') 69 | data = sp.generate(16) 70 | self.assertListEqual(['Xc', 'Xd', 'Xe', 'Xf'], data[-4:]) 71 | data = sp.generate_values(16) 72 | self.assertListEqual([12, 13, 14, 15], data[-4:]) 73 | 74 | def test_data_manage(self): 75 | sp = SerialNoPool(label_fmt='LC{no:04d}') 76 | sp.add_elements([0, 1, 2, 3]) 77 | data = sp.generate_labels(2) 78 | self.assertListEqual(['LC0004', 'LC0005'], data) 79 | sp.add_elements(data) 80 | sp.remove_elements([5]) 81 | data = sp.generate_labels(2) 82 | self.assertListEqual(['LC0005', 'LC0006'], data) 83 | 84 | with self.assertRaises(ValueError): 85 | sp.add_elements([-2]) 86 | with self.assertRaises(TypeError): 87 | sp.add_elements([2.5]) 88 | 89 | def test_elements_source(self): 90 | actual_store = ['LC0001', 'LC0002'] 91 | 92 | def get_source(): 93 | return actual_store 94 | 95 | sp = SerialNoPool(label_fmt='LC{no:04d}') 96 | sp.set_source(get_source) 97 | 98 | data = sp.generate_labels(2) 99 | self.assertListEqual(['LC0003', 'LC0004'], data) 100 | 101 | actual_store.extend(data) 102 | 103 | data = sp.generate_labels(2) 104 | self.assertListEqual(['LC0005', 'LC0006'], data) 105 | -------------------------------------------------------------------------------- /tests/test_singleton.py: -------------------------------------------------------------------------------- 1 | from borax.patterns.singleton import MetaSingleton 2 | 3 | 4 | class SingletonM(metaclass=MetaSingleton): 5 | pass 6 | 7 | 8 | def test_singleton(): 9 | a = SingletonM() 10 | b = SingletonM() 11 | assert id(a) == id(b) 12 | -------------------------------------------------------------------------------- /tests/test_string_convert.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import unittest 4 | from unittest.mock import Mock, patch 5 | 6 | from borax.strings import camel2snake, snake2camel, get_percentage_display 7 | from borax.system import rotate_filename, SUFFIX_DT_UNDERLINE 8 | 9 | FIXTURES = [ 10 | ('HelloWord', 'hello_word'), 11 | ('A', 'a'), 12 | ('Aa', 'aa'), 13 | ('Act', 'act'), 14 | ('AcTa', 'ac_ta') 15 | ] 16 | 17 | 18 | class StringConvertTestCase(unittest.TestCase): 19 | def test_all(self): 20 | for cs, ss in FIXTURES: 21 | with self.subTest(cs=cs, ss=ss): 22 | self.assertEqual(cs, snake2camel(ss)) 23 | self.assertEqual(ss, camel2snake(cs)) 24 | 25 | 26 | class PercentageStringTestCase(unittest.TestCase): 27 | def test_convert(self): 28 | self.assertEqual('100.00%', get_percentage_display(1, places=2)) 29 | self.assertEqual('56.23%', get_percentage_display(0.5623)) 30 | 31 | 32 | class FilenameRotateTestCase(unittest.TestCase): 33 | @patch('borax.system.datetime') 34 | def test_rotate(self, my_datetime): 35 | my_datetime.now = Mock(return_value=datetime(2019, 5, 25)) 36 | self.assertEqual( 37 | 'demo_20190525.docx', 38 | rotate_filename('demo.docx', time_fmt='%Y%m%d') 39 | ) 40 | self.assertEqual( 41 | 'demo-2019_05_25.docx', 42 | rotate_filename('demo.docx', time_fmt='%Y_%m_%d', sep='-') 43 | ) 44 | 45 | self.assertEqual( 46 | 'demo-2019_05_25.docx', 47 | rotate_filename('demo.docx', time_fmt='%Y_%m_%d', sep='-') 48 | ) 49 | # Full path 50 | self.assertEqual( 51 | '/usr/home/pi/demo-2019_05_25.docx', 52 | rotate_filename('/usr/home/pi/demo.docx', time_fmt='%Y_%m_%d', sep='-') 53 | ) 54 | # name with a dot char. 55 | self.assertEqual( 56 | '/usr/home/pi/bws-v3.2.1-upgrade_2019_05_25.sql', 57 | rotate_filename('/usr/home/pi/bws-v3.2.1-upgrade.sql', time_fmt='%Y_%m_%d', sep='_') 58 | ) 59 | 60 | def test_with_one_datetime(self): 61 | now = datetime(2019, 5, 23, 10, 22, 23) 62 | self.assertEqual( 63 | 'demo_20190523102223.docx', 64 | rotate_filename('demo.docx', now=now) 65 | ) 66 | self.assertEqual( 67 | 'demo-2019_05_23_10_22_23.docx', 68 | rotate_filename('demo.docx', time_fmt=SUFFIX_DT_UNDERLINE, sep='-', now=now) 69 | ) 70 | 71 | self.assertEqual( 72 | 'demo-2019_05_23.docx', 73 | rotate_filename('demo.docx', time_fmt='%Y_%m_%d', sep='-', now=now) 74 | ) 75 | # Full path 76 | self.assertEqual( 77 | '/usr/home/pi/demo-2019_05_23.docx', 78 | rotate_filename('/usr/home/pi/demo.docx', time_fmt='%Y_%m_%d', sep='-', now=now) 79 | ) 80 | # name with a dot char. 81 | self.assertEqual( 82 | '/usr/home/pi/bws-v3.2.1-upgrade_2019_05_23.sql', 83 | rotate_filename('/usr/home/pi/bws-v3.2.1-upgrade.sql', time_fmt='%Y_%m_%d', sep='_', now=now) 84 | ) 85 | -------------------------------------------------------------------------------- /tests/test_tree_fetcher.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from borax.structures.tree import pll2cnl, _parse_extra_data 4 | 5 | 6 | class TreeFetcherTestCase(unittest.TestCase): 7 | def test_parse_extra(self): 8 | source = {'id': 0, 'name': 'A', 'parent': None} 9 | extra_data = _parse_extra_data( 10 | source, 11 | flat_fields=[], 12 | extra_fields=[], 13 | extra_key=None, 14 | trs_fields=['id', 'parent'] 15 | ) 16 | self.assertDictEqual({'name': 'A'}, extra_data) 17 | extra_data = _parse_extra_data( 18 | source, 19 | flat_fields=[], 20 | extra_fields=['name'], 21 | extra_key="extra", 22 | trs_fields=['id', 'parent'] 23 | ) 24 | 25 | self.assertDictEqual({'extra': {'name': 'A'}}, extra_data) 26 | 27 | def test_pll2cnl(self): 28 | source = [ 29 | {'id': 0, 'name': 'A', 'parent': None}, 30 | {'id': 1, 'name': 'B', 'parent': 0}, 31 | {'id': 2, 'name': 'C', 'parent': 0}, 32 | {'id': 3, 'name': 'D', 'parent': 0}, 33 | {'id': 4, 'name': 'E', 'parent': 1}, 34 | {'id': 5, 'name': 'F', 'parent': 1}, 35 | {'id': 6, 'name': 'H', 'parent': 3}, 36 | {'id': 7, 'name': 'I', 'parent': 3}, 37 | {'id': 8, 'name': 'J', 'parent': 3}, 38 | ] 39 | 40 | children_list = pll2cnl(source) 41 | self.assertEqual(0, children_list[0]['id']) 42 | self.assertEqual('B', children_list[0]['children'][0]['name']) 43 | 44 | children_list1 = pll2cnl(source, flat_fields=['name']) 45 | self.assertEqual(0, children_list1[0]['id']) 46 | self.assertEqual('B', children_list1[0]['children'][0]['name']) 47 | 48 | self.assertSequenceEqual(children_list, children_list1) 49 | 50 | with self.assertRaises(ValueError): 51 | pll2cnl(source, extra_key='parent') 52 | with self.assertRaises(ValueError): 53 | pll2cnl(source, flat_fields=['parent']) 54 | with self.assertRaises(ValueError): 55 | pll2cnl(source, flat_fields=['foo']) 56 | 57 | def test_forward_reference(self): 58 | source = [ 59 | {'id': 0, 'name': 'A', 'parent': None}, 60 | {'id': 1, 'name': 'B', 'parent': 3}, 61 | {'id': 2, 'name': 'C', 'parent': 0}, 62 | {'id': 3, 'name': 'D', 'parent': 0}, 63 | ] 64 | data = pll2cnl(source) 65 | self.assertIsNotNone(data) 66 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import unittest 3 | from unittest.mock import Mock 4 | 5 | from borax.utils import force_iterable, trim_iterable, firstof, get_item_cycle, chain_getattr, force_list 6 | 7 | 8 | class BaseTestCase(unittest.TestCase): 9 | def test_force_iterable(self): 10 | self.assertListEqual([1], force_iterable(1)) 11 | self.assertListEqual([1, 2], force_iterable([1, 2])) 12 | 13 | def test_item_cycle(self): 14 | data = list(range(0, 10)) 15 | self.assertEqual(6, get_item_cycle(data, 6)) 16 | 17 | def test_item_cycle2(self): 18 | source = list(range(7)) 19 | total = 1232 20 | for index, ele in enumerate(itertools.cycle(source)): 21 | if index > total: 22 | break 23 | self.assertEqual(ele, get_item_cycle(source, index)) 24 | 25 | def test_chain(self): 26 | c = Mock(a=Mock(c='ac')) 27 | self.assertEqual('ac', chain_getattr(c, 'a.c')) 28 | d = object() 29 | self.assertEqual('default', chain_getattr(d, 'b.c', 'default')) 30 | 31 | 32 | class StringTrimTestCase(unittest.TestCase): 33 | def test_trim_list(self): 34 | # note: [4, 5, 6, 5, 1, 1,5] 35 | elements = ['1212', '34343', '783454', '23904', '2', '1', '30992'] 36 | 37 | expect = trim_iterable(elements, 10) 38 | self.assertListEqual(['1212', '34343'], expect) 39 | expect = trim_iterable(elements, 9) 40 | self.assertListEqual(['1212', '34343'], expect) 41 | expect = trim_iterable(elements, 3) 42 | self.assertListEqual([], expect) 43 | 44 | result = trim_iterable(elements, 20, split='/') 45 | self.assertEqual('1212/34343/783454', result) 46 | result = trim_iterable(elements, 9, split='/') 47 | self.assertEqual('1212', result) 48 | 49 | result = trim_iterable(elements, 18, split='', prefix='X') 50 | self.assertEqual('X1212X34343X783454', result) 51 | 52 | result = trim_iterable(elements, 18, split='-', prefix='X') 53 | self.assertEqual('X1212-X34343', result) 54 | 55 | 56 | class FirstofTestCase(unittest.TestCase): 57 | def test_first_of(self): 58 | self.assertEqual(3, firstof([None, None, 3, 4])) 59 | self.assertEqual(3, firstof([None, None, None, None], default=3)) 60 | 61 | 62 | class ForceListTestCase(unittest.TestCase): 63 | def test_force_list(self): 64 | self.assertTupleEqual((1, 2), force_list((1, 2))) 65 | self.assertTupleEqual(('1', '2'), force_list('1,2')) 66 | self.assertTupleEqual((1,), force_list(1)) 67 | -------------------------------------------------------------------------------- /tools/validate_lunar.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | """ 3 | Validate with Lunar-Solar-Calendar-Converter as dataset source. 4 | See more detail at https://github.com/isee15/Lunar-Solar-Calendar-Converter 5 | 6 | First install requirements 7 | 8 | pip install LunarSolarConverter 9 | """ 10 | import time 11 | from datetime import timedelta 12 | 13 | from LunarSolarConverter import LunarSolarConverter, Solar 14 | 15 | from borax.calendars.lunardate import LunarDate, MIN_SOLAR_DATE, MAX_SOLAR_DATE 16 | 17 | 18 | def iter_solar_date(): 19 | cur_date = MIN_SOLAR_DATE 20 | i = 0 21 | while cur_date <= MAX_SOLAR_DATE or i < 10: 22 | yield cur_date 23 | cur_date = cur_date + timedelta(days=1) 24 | i += 1 25 | 26 | 27 | converter = LunarSolarConverter() 28 | 29 | 30 | def validate_lunar(): 31 | t1 = time.time() 32 | total = 0 33 | fail = 0 34 | records = [] 35 | for sd in iter_solar_date(): 36 | 37 | ld = LunarDate.from_solar(sd) # test target 38 | actual = ld.strftime('%y,%m,%d,%l') 39 | 40 | solar = Solar(sd.year, sd.month, sd.day) 41 | solar_date_str = '{},{},{}'.format(sd.year, sd.month, sd.day) 42 | lunar = converter.SolarToLunar(solar) 43 | 44 | expected = '{},{},{},{}'.format(lunar.lunarYear, lunar.lunarMonth, lunar.lunarDay, int(lunar.isleap)) 45 | 46 | # solar_date_str = sd.strftime("%Y,%m,%d") 47 | # rsp = urllib.request.urlopen(url='http://localhost:1337/?src={}'.format(solar_date_str)) 48 | # expected = rsp.read().decode('utf8') 49 | 50 | total += 1 51 | if actual != expected: 52 | records.append('{} {} {}'.format(solar_date_str, expected, actual)) 53 | fail += 1 54 | if fail > 0: 55 | with open('fail_record.txt', 'w') as fp: 56 | fp.write('\n'.join(records)) 57 | t2 = time.time() 58 | print('Completed! total:{}, fail:{};time {}s'.format(total, fail, int(t2 - t1))) 59 | 60 | 61 | if __name__ == '__main__': 62 | validate_lunar() 63 | --------------------------------------------------------------------------------