首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >将OEIS序列转换为Desmos列表的工具

将OEIS序列转换为Desmos列表的工具
EN

Code Review用户
提问于 2020-07-07 18:39:39
回答 1查看 167关注 0票数 6

https://github.com/CascadeIllusion/DesmOEIS

最近开始了一个名为DesmOEIS的Python项目来构建我的投资组合。这是一个简单的控制台程序,它从OEIS中查找整数序列,解析它们,并使用Desmos API将它们转换为Desmos列表。

这个程序运行得很好,我只想得到一般的反馈,以确保我做的事情是正确的(不仅仅是在代码上,而且在项目结构方面)。单元测试在我的终端上成功了,但是也许我没有构建好的单元测试。

console.py

代码语言:javascript
复制
import sys
import webbrowser
from parsing import *
from desmos import *
from sequence import Sequence


def main():

    intro = \
      "DesmOEIS\n" \
      "A tool for converting OEIS sequences to Desmos lists.\n" \
      "\n" \
      "Type id=<OEIS id> (without the brackets) to convert a sequence. \n" \
      "Type help for a list of all valid commands. \n" \
      "Type exit to close the application. \n" \

    help = \
        "\nSyntax: (Command Name)=(Argument)\n\n" \
        "id: Attempts to convert an OEIS id argument into a Desmos list. \n" \
        "The \"A\" is optional, and trailing zeros may be excluded.\n\n" \
        \
        "name: Assigns the resulting Desmos list to a variable with the given name. \n" \
        "Names must be exactly one letter character (except \"e\"), no numbers or special characters.\n\n" \
        \
        "trim: Filters a list using Python-style slicing syntax. For A:B:C:\n" \
        "A is the starting index (inclusive), default 0.\n" \
        "B is the ending index (exclusive), default is the list length.\n" \
        "C is a step value that is used to skip every C elements, default is 1 (don't skip anything).\n\n" \
        \
        "ext: Pass Y to this to output the extended version of the OEIS sequence.\n" \
        "WARNING: Passing an entire extended sequence this way is usually not a good idea, as such\n" \
        "sequences can be hundreds of elements long, and can cause your browser to hang. You may want\n" \
        "to combine this with trimming syntax to reduce the number of elements.\n\n" \
        \
        "view: Opens the .html file containing the last converted sequence since starting the program. \n" \
        "Does not work if used before converting a sequence.\n\n" \
        \
        "help: View a list of all valid commands.\n\n" \
        \
        "exit: Closes the application." \

    print(intro)

    file = None

    while True:
        cmd = input()

        if cmd == "help":
            print(help)
            continue

        if cmd == "view":
            if file is None:
                print("No sequence converted yet.")
                continue
            webbrowser.open(f"file://{os.path.realpath(file)}")
            continue

        if cmd == "exit":
            sys.exit()

        # Multiple commands are comma separated
        cmds = cmd.split(', ')
        cmds[-1] = cmds[-1].replace(',', '')

        if not cmds[0].startswith("id"):
            print("First argument must be id.")
            continue

        args = dict()

        for i in cmds:
            i = i.split("=")

            cmd = i[0]
            value = i[1]

            args[cmd] = value

        id = parse_id(args)

        results = find_id(id)

        if results:
            sequence = Sequence(id)
            sequence.args = args
            sequence.results = results
        else:
            print("Invalid id.")
            continue

        sequence.integers = parse_integers(sequence)

        name = sequence.args.get("name")

        if name:
            if len(name) > 1:
                print("Variable names must be one character only.")
                continue

            if str.isdecimal(name) or name == 'e':
                print("Numeric names and the constant e (2.71828...) are not allowed.")
                continue

        sequence.name = name

        file = create_expression(sequence, create_desmos_list)

        print("Sequence converted successfully! \n")


if __name__ == '__main__':
    main()

parsing.py

代码语言:javascript
复制
import requests


def parse_id(args):

    id = args.get("id")

    # Remove the preceding A if included
    id = id.replace('A', '')

    # Add trailing zeros if necessary
    length = len(id)
    if length < 6:
        for i in range(0, 6 - length):
            id = "0" + id

    # Add A at the beginning of the query
    id = 'A' + id

    return id


def find_id(id):

    url = f"https://oeis.org/search?q=id:{id}&fmt=text"
    r = requests.get(url)

    if "No results." in r.text:
        return None

    return r


def parse_integers(sequence):

    text = sequence.results.text

    text = str.splitlines(text)

    rows = []

    if sequence.args.get("ext") == "Y":

        b_id = sequence.id.replace('A', 'b')
        url = f"https://oeis.org/{sequence.id}/{b_id}.txt"
        r = requests.get(url)
        sequence.results = r
        text = r.text
        text = str.splitlines(text)

        for line in text:
            space = line.find(" ")
            row = line[space + 1:]
            row = row.split(', ')
            rows.append(row)

    else:

        for line in text:
            if line.startswith('%S') or line.startswith('%T') or line.startswith('%U'):
                # integers start 11 characters into the line
                row = line[11:]
                row = row.split(',')
                rows.append(row)

    rows = [row for integer in rows for row in integer]

    # Remove empty elements resulting from commas at the end of the %S and %T rows
    rows = list(filter(None, rows))

    trim = sequence.args.get("trim")

    if trim:
        if ":" not in trim:
            print("Trim argument missing colons ( : ).")
            return
        trim = trim.split(":")
        if not (trim[0] is "" or trim[1] is ""):
            for i in trim:
                if i.isdigit() and trim[0] >= trim[1]:
                    print("Start value must be less than the end value.")
                    return
        if trim[0] is "":
            trim[0] = '0'
        if trim[1] is "":
            trim[1] = len(rows)
        for i in trim:
            i = str(i)
            if not i.isdigit():
                print("Invalid input for trim argument.")
                return
        trim = list(map(int, trim))
        start = trim[0]
        end = trim[1]
        if len(trim) == 3:
            if trim[2] is 0:
                print("Step value cannot be zero.")
                return
            step = trim[2]
            rows = rows[start:end:step]
        else:
            rows = rows[start:end]

    return rows

desmos.py

代码语言:javascript
复制
import os
from sequence import Sequence

def create_expression(sequence, func):

    graph = open("../resources/desmos_graph_base.html")
    graph = graph.read()

    expr = func(sequence)

    # Add another placeholder comment below the expression to allow for further expressions
    graph = graph.replace("<!-- PLACEHOLDER -->", f"{expr} \n <!-- PLACEHOLDER -->")

    sequence.graph = graph

    dir = "../graphs/"
    if not os.path.exists(dir):
        os.makedirs(dir)
    filename = f"{dir}{sequence.id}.html"
    out_graph = open(filename, "w")
    out_graph.write(graph)

    return filename


def create_desmos_list(sequence):

    integers = sequence.integers

    desmos_list = str(integers)
    desmos_list = desmos_list.replace("'", "")

    name = sequence.name

    if sequence.args.get("name") is not None:
        name = name + "="
    else:
        name = ""

    expr = f"calculator.setExpression({{ id: 'graph1', latex:\"{name}{desmos_list}\" }});"
    return expr

sequence.py

代码语言:javascript
复制
class Sequence():

    def __init__(self, id):
        self._id = id

    @property
    def id(self):
        return self._id

    @property
    def args(self):
        return self._args

    @args.setter
    def args(self, args):
        self._args = args

    @property
    def integers(self):
        return self._integers

    @integers.setter
    def integers(self, integers):
        self._integers = integers

    @property
    def results(self):
        return self._results

    @results.setter
    def results(self, results):
        self._results = results

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

test_parsing.py

代码语言:javascript
复制
import unittest
from parsing import *
from sequence import *

"""
# Example sequences used:
http://oeis.org/A000045
http://oeis.org/A000047
http://oeis.org/A139827
"""


class TestIdParsing(unittest.TestCase):

    def test_parse_id(self):
        args = {"id": "A000045"}
        self.assertEqual("A000045", parse_id(args))

    def test_parse_id_no_prefix(self):
        args = {"id": "000045"}
        self.assertEqual("A000045", parse_id(args))

    def test_parse_id_no_trailing_zeros(self):
        args = {"id": "A45"}
        self.assertEqual("A000045", parse_id(args))

    def test_parse_id_no_trailing_zeros_no_prefix(self):
        args = {"id": "45"}
        self.assertEqual("A000045", parse_id(args))

    def test_parse_id_six_digit(self):
        args = {"id": "A139827"}
        self.assertEqual("A139827", parse_id(args))

    def test_parse_id_six_digit_no_prefix(self):
        args = {"id": "139827"}
        self.assertEqual("A139827", parse_id(args))


class TestIdFinding(unittest.TestCase):

    def test_find_id_success(self):
        id = "A000045"
        self.assertNotEqual(None, find_id(id))

    def test_find_id_fail(self):
        id = "A123ABC"
        self.assertEqual(None, find_id(id))


class TestIntegerParsing(unittest.TestCase):

    def test_parse_integers(self):

        args = {"": ""}

        id = "A000045"

        integers = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,
                    17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578,
                    5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155]

        integers = list(map(str, integers))

        url = f"https://oeis.org/search?q=id:{id}&fmt=text"
        r = requests.get(url)

        sequence = Sequence(id)
        sequence.integers = integers
        sequence.results = r
        sequence.args = args

        self.assertEqual(integers, parse_integers(sequence))

    # Use a different sequence because most extended sequences are too big to reasonably fit
    def test_parse_integers_ext(self):

        args = {"ext": "Y"}

        id = "A000047"

        integers = [1, 2, 3, 5, 8, 15, 26, 48, 87, 161, 299, 563, 1066, 2030, 3885, 7464, 14384, 27779, 53782, 104359,
                    202838, 394860, 769777, 1502603, 2936519, 5744932, 11249805, 22048769, 43248623, 84894767,
                    166758141, 327770275, 644627310, 1268491353, 2497412741, 4919300031, 9694236886, 19112159929]
        integers = list(map(str, integers))

        url = f"https://oeis.org/search?q=id:{id}&fmt=text"
        r = requests.get(url)

        sequence = Sequence(id)
        sequence.integers = integers
        sequence.results = r
        sequence.args = args

        self.assertEqual(integers, parse_integers(sequence))


if __name__ == '__main__':
    unittest.main()
EN

回答 1

Code Review用户

回答已采纳

发布于 2020-07-07 19:05:58

线连续

像这样的多行字符串:

代码语言:javascript
复制
intro = \
  "DesmOEIS\n" \
  "A tool for converting OEIS sequences to Desmos lists.\n" \
  "\n" \
  "Type id=<OEIS id> (without the brackets) to convert a sequence. \n" \
  "Type help for a list of all valid commands. \n" \
  "Type exit to close the application. \n" \

更好地表示为括号的表达式:

代码语言:javascript
复制
intro = (
    "DesmOEIS\n" 
    "A tool for converting OEIS sequences to Desmos lists.\n" 
    "\n" 
    "Type id=<OEIS id> (without the brackets) to convert a sequence.\n" 
    "Type help for a list of all valid commands.\n" 
    "Type exit to close the application.\n" 
)

还要注意的是,缩进的第二层应该是四个空格,而不是两个。

考虑不区分大小写的命令

将字符串与降低的输入进行比较是很容易的,也是用户生活质量的提高:

代码语言:javascript
复制
cmd = input().lower()

如果您想得到真正的幻想,您可以实现明确的前缀字符串匹配,即hel将匹配help,但这是更高级的。

列表解析

而不是这样:

代码语言:javascript
复制
    # Multiple commands are comma separated
    cmds = cmd.split(', ')

如果只在逗号上拆分,然后删除每个结果条目,就更安全了。

字典文字

代码语言:javascript
复制
args = dict()

可以是

代码语言:javascript
复制
args = {}

解包装

代码语言:javascript
复制
        i = i.split("=")

        cmd = i[0]
        value = i[1]

可以是

代码语言:javascript
复制
cmd, value = i.split('=')

替换或修剪

代码语言:javascript
复制
# Remove the preceding A if included
id = id.replace('A', '')

不会照你说的做。它替换字符串中任何位置的“A”。相反,考虑一下lstrip('A'),它从左边删除任意数量的'A‘,或者如果您想更精确地使用正则表达式和'^A'

字符重复

代码语言:javascript
复制
    for i in range(0, 6 - length):
        id = "0" + id

可以是

代码语言:javascript
复制
id = '0'*(6 - length) + id

请求参数

代码语言:javascript
复制
url = f"https://oeis.org/search?q=id:{id}&fmt=text"
r = requests.get(url)

应该对params使用dict参数,而不是将其烘焙到URL中;请阅读https://2.python-requests.org/en/master/user/quickstart/#passing-parameters-in-urls

请求检查失败

在你的r.raise_for_status()之后打电话给get。否则,HTTP请求的失败模式将被隐藏。

路径库

考虑一下这些调用的pathlib.Path替代方案:

代码语言:javascript
复制
if not os.path.exists(dir):
    os.makedirs(dir)

设置器和getter

这种样板:

代码语言:javascript
复制
@property
def args(self):
    return self._args

@args.setter
def args(self, args):
    self._args = args

是非丙酮的。除了约定之外,_args不是一个严格的私有变量。你不能从这些属性中得到任何东西。如果要允许用户修改该成员,请删除它们并将_args重命名为args

单元测试

太好了,你有一些!考虑在您的测试中模拟掉requests.get;否则,这不能严格地被视为一个单元测试。

票数 4
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/245151

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档