openlang

`isdigit()`는 통과시키고 `int()`는 거부한 위첨자 2

작은 폼의 수량 입력란이 s.isdigit()로 검사한 다음 int(s)를 불러요. 누군가 ²를 붙여 넣었더니, 바로 그 검사가 지켜 주기로 한 줄에서 ValueError가 났어요.

둘은 서로 다른 규칙을 써요. isdigit()은 숫자 값을 가진 문자면 참이라서 위첨자나 ⑦ 같은 동그라미 숫자까지 받아들여요. int()는 십진 숫자, 즉 isdecimal() 집합을 원하고요. 그래서 ²는 첫 번째는 통과하고 두 번째에서 실패해요.

for s in ["7", "²", "٣", "3"]:
    print(repr(s), s.isdigit(), s.isdecimal())
print(int("٣"), int("3"))  # 3 3

검사를 isdecimal()로 바꾸면 크래시는 멈춰요. 그런데 아랍-인도 숫자 ٣와 전각 3도 3으로 통과시키게 되는데, 이 입력란은 그런 문자를 선택지로 보여 준 적이 없어요. 크래시는 고칠 방법이 뻔하지만, 입력란이 그런 문자를 받아야 하는지는 아직 잘 모르겠어요. 다들 숫자 입력에 무엇까지 허용하세요?

이어지는 대화 4

서로의 근거를 따라
  1. 고유 링크

    재미있는 구석이 있어요. int()는 숫자들이 같은 문자 체계에서 왔는지 신경 쓰지 않아요. int("1٣")은 ASCII 숫자 하나에 아랍-인도 숫자 하나인데 13이 되고, float("١.٥")는 1.5가 나와요. 혼동 문자 검사처럼 섞인 문자 체계를 거부할 줄 알았는데, 그냥 문자마다 숫자 값을 읽어서 합칠 뿐이에요.

    그러니까 isdecimal()로 가면 어떤 키보드 배열로도 한 번에 칠 수 없는 숫자를 입력란이 받아요. 1٣을 붙여 넣고 폼이 뭘 다시 보여 주는지 봐 보세요.

  2. 고유 링크

    덕분에 정했어요. ASCII 숫자만 받기로요. 처음에는 re.match(r"\d+$", s)로 했는데, 바로 알아채지 못한 문제가 두 개 있었어요. str 패턴에서 \d는 유니코드 십진 숫자를 전부 맞춰서 ٣가 여전히 들어왔어요. 그리고 $는 끝의 줄바꿈 바로 앞에서도 맞아서 "7\n"도 통과했고요. 지금 입력란은 이걸 써요.

    import re
    
    def parse_quantity(s: str) -> int | None:
        if re.fullmatch(r"[0-9]+", s):
            return int(s)
        return None

    명시적인 문자 클래스와 fullmatch를 쓰면 질문이 하나로 줄어요. 처음부터 끝까지 ASCII 숫자인가, 그것뿐이에요. 공백, 부호, 밑줄도 거부돼요. int() 자체는 " 7 "이나 "1_000"도 받아 주는데도요.

  3. 고유 링크

    fullmatch에는 동의해요. $가 줄바꿈 앞에서 맞는 건 늘 사람들을 걸려 넘어지게 하니까요. 전각 숫자를 거부하는 건 좀 덜 확신이 가요. 일본어 입력기를 전각 모드로 둔 채 입력하는 사람은 모르는 사이에 3을 치게 되고, 그 사람한테 오류는 폼이 숫자 3을 거부하는 걸로 읽혀요. 항목 열 개짜리 표 하나면 isdecimal()이 허용하는 걸 전부 열어 주지 않고도 그 경우 하나는 처리돼요.

    FULLWIDTH = str.maketrans("0123456789", "0123456789")
    parse_quantity(s.translate(FULLWIDTH))

    위첨자나 다른 문자 체계는 여전히 거부돼요. 전각만 예외로 둘 만한지는 판단의 문제라서, 이 답글 이상으로 밀어붙일 만큼 확신이 있지는 않아요.

  4. 고유 링크

    실제로 있을 만한 경우고 표도 작아요. 그래도 지금은 넣지 않을래요. 입력란 옆에 더하기, 빼기 버튼이 있고 오류 문구가 "0부터 9까지의 숫자를 쓰세요"라고 말해 주니까요.

같은 질문의 다른 갈래

  1. 이름 조회에 NFC 키를 기본으로 써도 될까요?

    자, 사용자가 이름을 붙여 넣었는데 끝이 e 다음에 U+0301이에요. 나중에 같은 이름을 직접 치면 키보드가 U+00E9를 넣어 줘요. 그 사람한테는 같은 이름이고 캐노니컬 등가이기도 한데, 문자열로는 여전히 달라요. 제…

  2. NFC 조회 키와 JCS 서명

    kim_dh가 앞서 올린 NFC 조회 키 질문은 제출된 이름과 NFC 조회 키를 나눠서 봤는데, 서명이 끼면 더 복잡해져요. JSON 페이로드의 이름이 e 뒤에 U+0301을 붙인 형태이고, 그 문자열이 JCS 기반 서명에…

  3. C++처럼 바이트 단위로 자르다가 만난 Rust panic

    오래된 C++ 코드를 Rust로 옮기고 있는데, std::string을 바이트 한도에 맞춰 s.substr(0, n)으로 자르는 부분이 있어요. 한 줄이면 될 줄 알았죠. C++에서는 그럭저럭 돌아갔어요. std::strin…

    답글 6rustcpp