ER 다이어그램 설계 연습 문서의 문제 2 풀이 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

"""ER 다이어그램 설계 연습 p2: MOVIE.

문서: 31.ER 다이어그램 설계 연습 (문제 2)
확인하는 것:
  1. 겹선을 MOVIE 쪽 둘에만 그은 그림의 숫자와 선을 (min, max)로 옮기면 PRODUCES는 1:N, 나머지 셋은 M:N이다.
  2. 요구사항의 "one or more" 문장 여섯 개 가운데 그 그림의 겹선이 지켜 주는 것은 둘뿐이다
     (영화마다 제작사 하나, 영화마다 감독 한 명 이상).
     나머지 넷(영화마다 배우 한 명 이상, 배우는 한 편 이상 출연, 감독은 한 편 이상 연출,
     제작사는 한 편 이상 제작)을 어긴 자료가 그 그림의 규칙은 통과한다.
  3. 요구사항대로 겹선을 더 그은 판은 그 넷을 모두 걸러 낸다.
"""
from collections import Counter

N = float("inf")
# 틀리기 쉬운 그림: 겹선은 MOVIE-PRODUCES, MOVIE-DIRECTS 둘뿐이다
SLIDE = {
    "PRODUCES": [("PRODUCTION", (0, N)), ("MOVIE", (1, 1))],
    "DIRECTS": [("DIRECTOR", (0, N)), ("MOVIE", (1, N))],
    "ACTS": [("ACTOR", (0, N)), ("MOVIE", (0, N))],
    "DIR_ACTS": [("DIRECTOR", (0, N)), ("MOVIE", (0, N))],
}
# 요구사항의 "one or more"를 모두 옮긴 판 (문서의 답)
BY_REQUIREMENT = dict(SLIDE,
                      PRODUCES=[("PRODUCTION", (1, N)), ("MOVIE", (1, 1))],
                      DIRECTS=[("DIRECTOR", (1, N)), ("MOVIE", (1, N))],
                      ACTS=[("ACTOR", (1, N)), ("MOVIE", (1, N))])


def violations(schema, entities, rels):
    out = []
    for rel, roles in schema.items():
        for i, (etype, (lo, hi)) in enumerate(roles):
            count = Counter(t[i] for t in rels.get(rel, []))
            out += [(rel, etype, e, count[e]) for e in entities[etype] if not lo <= count[e] <= hi]
    return out


def ratio(roles):
    (_, a), (_, b) = roles
    left, right = ("1" if b[1] == 1 else "N"), ("1" if a[1] == 1 else "N")
    return "M:N" if (left, right) == ("N", "N") else f"{left}:{right}"


def main():
    assert ratio(SLIDE["PRODUCES"]) == "1:N"
    assert [ratio(SLIDE[r]) for r in ("DIRECTS", "ACTS", "DIR_ACTS")] == ["M:N"] * 3
    print("[OK] PRODUCES는 제작사 1 : 영화 N, DIRECTS·ACTS·DIR_ACTS는 M:N")

    ents = {"PRODUCTION": ["c1"], "MOVIE": ["m1"], "DIRECTOR": ["d1"], "ACTOR": ["a1"]}
    rels = {"PRODUCES": [("c1", "m1")], "DIRECTS": [("d1", "m1")], "ACTS": [("a1", "m1")], "DIR_ACTS": []}
    assert violations(SLIDE, ents, rels) == [] and violations(BY_REQUIREMENT, ents, rels) == []

    # MOVIE 쪽에만 겹선을 그은 그림이 지켜 주는 두 문장
    no_company = dict(rels, PRODUCES=[])
    assert ("PRODUCES", "MOVIE", "m1", 0) in violations(SLIDE, ents, no_company)
    no_director = dict(rels, DIRECTS=[])
    assert ("DIRECTS", "MOVIE", "m1", 0) in violations(SLIDE, ents, no_director)
    print("[OK] MOVIE 쪽에만 겹선을 그은 그림: 제작사 없는 영화, 감독 없는 영화는 걸린다")

    # 그 그림이 못 지키는 네 문장
    cases = {
        "배우 없는 영화": (ents, dict(rels, ACTS=[]), ("ACTS", "MOVIE", "m1", 0)),
        "출연작 없는 배우": (dict(ents, ACTOR=["a1", "a2"]), rels, ("ACTS", "ACTOR", "a2", 0)),
        "연출작 없는 감독": (dict(ents, DIRECTOR=["d1", "d2"]), rels, ("DIRECTS", "DIRECTOR", "d2", 0)),
        "제작한 영화 없는 제작사": (dict(ents, PRODUCTION=["c1", "c2"]), rels, ("PRODUCES", "PRODUCTION", "c2", 0)),
    }
    for name, (e, r, expect) in cases.items():
        assert violations(SLIDE, e, r) == [], name
        assert expect in violations(BY_REQUIREMENT, e, r), name
    print("[OK] 네 가지 자료는 MOVIE 쪽에만 겹선을 그은 그림을 통과하고, 요구사항대로 그은 판에서는 모두 걸린다")

    # 감독이 배우로 나오는 것은 '할 수도 있다'라서 DIR_ACTS는 양쪽 다 부분 참여여야 한다
    assert SLIDE["DIR_ACTS"] == BY_REQUIREMENT["DIR_ACTS"] == [("DIRECTOR", (0, N)), ("MOVIE", (0, N))]
    print("[OK] DIR_ACTS는 두 판 모두 양쪽 부분 참여")

    print("ALL CHECKS PASSED")


if __name__ == "__main__":
    main()