속성의 종류 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""속성의 종류 검증.

문서: 20.속성의 종류 (예시로 보기, 정의, 카드 C1·C3)
출처: Elmasri & Navathe, Fundamentals of Database Systems 3장의 속성 예와 COMPANY 상태(직원의 생일과 부서 번호)
주장:
  1. 문서의 Address 계층에서 더 못 쪼개는 속성은 6개다: Number, Street, Apartment_number, City, State, Zip.
  2. 글 표기에서 괄호는 복합 속성, 중괄호는 다치 속성이다.
     CAR: 복합 1개(Registration), 다치 1개(Color), 더 못 쪼개는 속성 7개.
     EMPLOYEE 처음 설계: Works_on은 복합이면서 다치다.
  3. CAR 세 대의 Color 값은 2개, 1개, 2개다(문서의 표). 다치 속성은 값이 여럿일 수 있다.
  4. 나이는 생일에서 계산된다. 같은 생일이라도 날짜가 지나면 값이 바뀐다(유도 속성으로 두는 까닭).
  5. 부서의 직원 수(Number_of_employees)는 소속을 세면 나온다: 5번 4명, 4번 3명, 1번 1명.
"""
from datetime import date


def parse(text):
    """'A(B, C), D, {E}' 꼴을 [(이름, 다치인가, 성분 목록)]으로 바꾼다. 성분 목록이 비면 단순 속성."""
    pos = 0

    def skip():
        nonlocal pos
        while pos < len(text) and text[pos] in " ,":
            pos += 1

    def attr():
        nonlocal pos
        skip()
        multi = text[pos] == "{"
        if multi:
            pos += 1
        start = pos
        while pos < len(text) and (text[pos].isalnum() or text[pos] == "_"):
            pos += 1
        name = text[start:pos]
        parts = []
        if pos < len(text) and text[pos] == "(":
            pos += 1
            parts = attrs(")")
            pos += 1
        if multi:
            assert text[pos] == "}"
            pos += 1
        return (name, multi, parts)

    def attrs(closer):
        out = []
        skip()
        while pos < len(text) and text[pos] != closer:
            out.append(attr())
            skip()
        return out

    return attrs("\0")


def leaves(tree):
    """더 못 쪼개는(단순) 속성 이름들."""
    out = []
    for name, _, parts in tree:
        out += leaves(parts) if parts else [name]
    return out


def composites(tree):
    out = []
    for name, _, parts in tree:
        if parts:
            out += [name] + composites(parts)
    return out


def multivalued(tree):
    out = []
    for name, multi, parts in tree:
        out += ([name] if multi else []) + multivalued(parts)
    return out


def age(birth, today):
    return today.year - birth.year - ((today.month, today.day) < (birth.month, birth.day))


def main():
    address = parse("Address(Street_address(Number, Street, Apartment_number), City, State, Zip)")
    assert leaves(address) == ["Number", "Street", "Apartment_number", "City", "State", "Zip"]
    assert composites(address) == ["Address", "Street_address"]
    print("[OK] Address 계층의 단순 속성 6개, 복합 속성 2개(Address, Street_address)")

    car = parse("Registration(Number, State), Vehicle_id, Make, Model, Year, {Color}")
    assert composites(car) == ["Registration"] and multivalued(car) == ["Color"]
    assert len(leaves(car)) == 7
    print("[OK] CAR의 복합 1개, 다치 1개, 단순 속성 7개")

    emp = parse("Name(Fname, Minit, Lname), Ssn, Sex, Address, Salary, Birth_date, "
                "Department, Supervisor, {Works_on(Project, Hours)}")
    assert composites(emp) == ["Name", "Works_on"] and multivalued(emp) == ["Works_on"]
    print("[OK] Works_on은 복합이면서 다치다")

    # 문서 표의 CAR 세 대의 Color
    colors = [{"red", "black"}, {"blue"}, {"white", "blue"}]
    assert [len(c) for c in colors] == [2, 1, 2]
    print("[OK] Color 값의 수 2, 1, 2")

    # 문서의 e1: Address = 2311 Kirby, Houston, Texas 77001 을 성분으로 나눠 다시 붙인다
    e1 = {"Number": "2311", "Street": "Kirby", "Apartment_number": None,
          "City": "Houston", "State": "Texas", "Zip": "77001"}
    street_address = " ".join(v for v in (e1["Number"], e1["Street"], e1["Apartment_number"]) if v)
    assert f'{street_address}, {e1["City"]}, {e1["State"]} {e1["Zip"]}' == "2311 Kirby, Houston, Texas 77001"
    print("[OK] 성분을 붙이면 원래 주소가 된다")

    # 유도 속성: 나이. John Smith의 생일 1965-01-09 (COMPANY 상태)
    birth = date(1965, 1, 9)
    assert age(birth, date(2024, 10, 4)) == 59 and age(birth, date(2025, 1, 9)) == 60
    assert age(birth, date(2025, 1, 8)) == 59
    print("[OK] 생일 1965-01-09: 2024-10-04에 59세, 2025-01-09에 60세")

    # 유도 속성: 부서의 직원 수. 직원 8명의 부서 번호 (COMPANY 상태)
    dno = [5, 5, 4, 4, 5, 5, 4, 1]
    count = {d: dno.count(d) for d in sorted(set(dno), reverse=True)}
    assert count == {5: 4, 4: 3, 1: 1}
    print("[OK] COMPANY 상태: 부서별 직원 수 5번 4명, 4번 3명, 1번 1명")

    print("ALL CHECKS PASSED")


if __name__ == "__main__":
    main()