"""記事 legacy-master-field-overflow の集計(商いの帳場)。
国税庁 法人番号公表サイトの「全件データ」(CSV形式・Unicode、全国)の商号と所在地を、
B社の公開仕様の入力桁数(得意先名称 全角40文字=半角80、住所1・住所2 各全角20文字=半角40)に当てて、
あふれる件数と割合を数える。

再現の方法:
  1. https://www.houjin-bangou.nta.go.jp/download/zenken/ から「CSV形式・Unicode」の「全国」の zip を落とす
     (この記事は 00_zenkoku_all_20260930.zip、令和8年9月30日更新のファイル)
  2. python scripts/figs/master_field_overflow.py <zip のパス>
出力:標準出力と scripts/figs/out/legacy-master-field-overflow.json、
     public/img/articles/legacy-master-field-overflow-{bars,addr,split}.svg(図3枚。split は引数なしでも作る)

列の意味は国税庁「リソース定義書(ダウンロードファイル、Web-API)」4.1版:
  7列目 商号又は名称(最大150字)、9列目 法人種別、10〜12列目 所在地(都道府県・市区町村・丁目番地等)、
  8列目 商号イメージID(JIS第1・第2水準以外の文字か150字超のとき入る)、19列目 登記記録の閉鎖等年月日。
桁の数え方:Shift_JIS と同じく、ASCII と半角カナを1、それ以外を2として数える(B社の「全角40文字(半角80文字)」に合わせる)。
数える対象:法人種別 301〜305(株式会社・有限会社・合名・合資・合同会社)で、閉鎖等年月日が空の法人。全種別の値も参考に出す。
出典の記載:国税庁法人番号公表サイト(国税庁)の全件データを加工して商いの帳場が作成。
"""
import csv, io, json, pathlib, re, sys, zipfile, platform
sys.stdout.reconfigure(encoding='utf-8')
ROOT = pathlib.Path(__file__).resolve().parents[2]
NAME_MAX, ADDR_LINE, ADDR_MAX = 80, 40, 80   # 半角換算
COMPANY = {'301', '302', '303', '304', '305'}

def w(s):
    return sum(1 if (ord(c) < 0x80 or 0xFF61 <= ord(c) <= 0xFF9F) else 2 for c in s)

def cut(s, limit):  # 先頭から limit(半角換算)に収まる長さ
    t = 0
    for i, c in enumerate(s):
        t += w(c)
        if t > limit: return i
    return len(s)

DIGIT = re.compile(r'[0-9０-９一二三四五六七八九十〇]')
def split_breaks_number(addr):  # 住所1(全角20)で機械的に切ると、数字の途中で割れるか
    i = cut(addr, ADDR_LINE)
    if i >= len(addr) or i == 0: return False
    return bool(DIGIT.match(addr[i - 1]) and DIGIT.match(addr[i]))

def run(path):
    z = zipfile.ZipFile(path)
    name = [n for n in z.namelist() if n.endswith('.csv')][0]
    st = {k: {'n': 0, 'nameOver': 0, 'nameOverAbbrevFits': 0, 'addrOver1': 0, 'addrOver2': 0,
              'splitBreaksNumber': 0, 'cityBoundaryFits': 0, 'nameImage': 0, 'nameHist': [0] * 7, 'addrHist': [0] * 7}
          for k in ('company', 'all')}
    bins = [20, 30, 40, 50, 60, 80]  # 全角換算の文字数の区切り
    def b(x):
        for i, e in enumerate(bins):
            if x <= e: return i
        return len(bins)
    with z.open(name) as f:
        for row in csv.reader(io.TextIOWrapper(f, encoding='utf-8', newline='')):
            nm, kind, closed = row[6], row[8], row[18]
            addr = row[9] + row[10] + row[11]
            keys = ['all'] + (['company'] if kind in COMPANY and not closed else [])
            wn, wa = w(nm), w(addr)
            for k in keys:
                s = st[k]; s['n'] += 1
                s['nameHist'][b((wn + 1) // 2)] += 1
                s['addrHist'][b((wa + 1) // 2)] += 1
                if row[7]: s['nameImage'] += 1
                if wn > NAME_MAX:
                    s['nameOver'] += 1
                    if w(nm.replace('株式会社', '(株)').replace('有限会社', '(有)').replace('合同会社', '(同)')) <= NAME_MAX:
                        s['nameOverAbbrevFits'] += 1
                if wa > ADDR_LINE:
                    s['addrOver1'] += 1
                    if split_breaks_number(addr): s['splitBreaksNumber'] += 1
                    # 住所1=都道府県+市区町村、住所2=丁目番地等 で分けて、両方が全角20に入るか
                    if w(row[9] + row[10]) <= ADDR_LINE and w(row[11]) <= ADDR_LINE: s['cityBoundaryFits'] += 1
                if wa > ADDR_MAX: s['addrOver2'] += 1
    for s in st.values():
        n = s['n']
        for k in ('nameOver', 'addrOver1', 'addrOver2', 'splitBreaksNumber', 'cityBoundaryFits', 'nameImage'):
            s[k + 'Pct'] = round(s[k] / n * 100, 2)
        s['addrOver1Per1000'] = round(s['addrOver1'] / n * 1000, 1)
    return {'file': name, 'python': platform.python_version(), 'bins': bins, 'stats': st}

FONT = "'Hiragino Sans','Yu Gothic','Meiryo',sans-serif"
def svg_hist(s, key, title, sid, limit_bin, note, limit_label):
    labels = ['〜20字', '21〜30', '31〜40', '41〜50', '51〜60', '61〜80', '81字〜']
    h = s[key]; n = s['n']; W, H = 720, 300; x0, bw, top, base = 70, 86, 60, 230
    mx = max(h)
    t = [f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {W} {H}' width='{W}' height='{H}' role='img' aria-labelledby='{sid}' font-family=\"{FONT}\" style='max-width:100%;height:auto'>",
         f"<title id='{sid}'>{title}</title><rect width='{W}' height='{H}' fill='#fff'/>",
         f"<text x='20' y='28' font-size='14' font-weight='700' fill='#23221f'>{note}</text>"]
    for i, v in enumerate(h):
        hh = v / mx * (base - top); x = x0 + i * bw
        col = '#678b2c' if i > limit_bin else '#c9c6bf'
        t.append(f"<rect x='{x + 10}' y='{base - hh:.1f}' width='{bw - 20}' height='{max(hh, 1):.1f}' fill='{col}'/>")
        lab = f'{v / n * 100:.2f}%' if v / n >= 0.001 else f'{v:,}件'
        t.append(f"<text x='{x + bw / 2}' y='{base - hh - 8:.1f}' font-size='11.5' font-weight='700' fill='#23221f' text-anchor='middle'>{lab}</text>")
        t.append(f"<text x='{x + bw / 2}' y='{base + 18}' font-size='11.5' fill='#4c4c4c' text-anchor='middle'>{labels[i]}</text>")
    xl = x0 + (limit_bin + 1) * bw
    t.append(f"<line x1='{xl}' x2='{xl}' y1='{top - 10}' y2='{base + 4}' stroke='#557323' stroke-width='2' stroke-dasharray='5 4'/>")
    t.append(f"<text x='{xl + 6}' y='{top}' font-size='12' font-weight='700' fill='#557323'>{limit_label}</text>")
    t.append(f"<text x='20' y='{H - 14}' font-size='10.5' fill='#706d65'>国税庁法人番号公表サイト(国税庁)の全件データ(2026年9月30日更新)を加工して商いの帳場が作成。会社{n:,}件、全角換算</text></svg>")
    return ''.join(t)

def svg_bars(rows, sid, title, head, foot):
    W, H = 720, 80 + 46 * len(rows) + 30; x0, x1 = 270, 520
    mx = max(r[1] for r in rows)
    t = [f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {W} {H}' width='{W}' height='{H}' role='img' aria-labelledby='{sid}' font-family=\"{FONT}\" style='max-width:100%;height:auto'>",
         f"<title id='{sid}'>{title}</title><rect width='{W}' height='{H}' fill='#fff'/>",
         f"<text x='20' y='28' font-size='14' font-weight='700' fill='#23221f'>{head}</text>"]
    for i, (lab, v, pct, col) in enumerate(rows):
        y = 52 + i * 46; bwid = v / mx * (x1 - x0)
        t.append(f"<text x='{x0 - 12}' y='{y + 20}' font-size='12.5' fill='#23221f' text-anchor='end'>{lab}</text>")
        t.append(f"<rect x='{x0}' y='{y + 4}' width='{max(bwid, 1):.1f}' height='24' fill='{col}'/>")
        t.append(f"<text x='{x0 + bwid + 8:.1f}' y='{y + 21}' font-size='12.5' font-weight='700' fill='#23221f'>{v:,}件{pct}</text>")
    t.append(f"<text x='20' y='{H - 14}' font-size='10.5' fill='#706d65'>{foot}</text></svg>")
    return ''.join(t)

def svg_split():  # 図3:架空の住所を2通りに分けた例(データは使わない)
    a, b = '神奈川県相模原市中央区', '架空町三丁目１２３４番地５'; full = a + b
    i = cut(full, ADDR_LINE); l1, l2 = full[:i], full[i:]
    def box(y, label, t1, t2):
        s = f"<text x='24' y='{y}' font-size='13' font-weight='700' fill='#23221f'>{label}</text>"
        for k, (lab, t) in enumerate((('住所1', t1), ('住所2', t2))):
            yy = y + 14 + k * 40
            s += f"<rect x='24' y='{yy}' width='70' height='30' fill='#f3f2ee'/><text x='59' y='{yy + 20}' font-size='12' fill='#4c4c4c' text-anchor='middle'>{lab}</text>"
            s += f"<rect x='94' y='{yy}' width='440' height='30' fill='#fff' stroke='#c9c6bf'/><text x='106' y='{yy + 21}' font-size='15' fill='#23221f'>{t}</text>"
        return s
    return (f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 720 330' width='720' height='330' role='img' aria-labelledby='lmfo-split' font-family=\"{FONT}\">"
            f"<title id='lmfo-split'>架空の住所(全角{len(full)}文字)を住所1と住所2に分けた2通りの例。全角20文字で切ると番地の１２３４が割れ、市区町村の境で切ると{len(a)}文字と{len(b)}文字に分かれる</title>"
            "<rect width='720' height='330' fill='#fff'/>"
            + box(30, '全角20文字で機械的に切る', l1, l2)
            + "<text x='548' y='86' font-size='12.5' font-weight='700' fill='#9a3b2a'>番地の「１２３４」が</text><text x='548' y='104' font-size='12.5' font-weight='700' fill='#9a3b2a'>2行に割れる</text>"
            + box(170, '市区町村の境で切る(都道府県+市区町村/丁目番地等)', a, b)
            + f"<text x='548' y='226' font-size='12.5' font-weight='700' fill='#557323'>どちらも全角20文字以内</text><text x='548' y='244' font-size='12.5' fill='#4c4c4c'>({len(a)}文字/{len(b)}文字)</text>"
            + "<text x='24' y='316' font-size='10.5' fill='#706d65'>住所は架空。上限は B社の公開仕様の住所1・住所2(各全角20文字)。商いの帳場が作成</text></svg>")

if __name__ == '__main__':
    (ROOT / 'public/img/articles/legacy-master-field-overflow-split.svg').write_text(svg_split(), encoding='utf-8')
    if len(sys.argv) < 2: sys.exit(0)  # 引数なしは図3だけ
    r = run(sys.argv[1])
    out = ROOT / 'scripts/figs/out'; out.mkdir(parents=True, exist_ok=True)
    (out / 'legacy-master-field-overflow.json').write_text(json.dumps(r, ensure_ascii=False, indent=1), encoding='utf-8')
    print(json.dumps(r, ensure_ascii=False, indent=1))
    s = r['stats']['company']; img = ROOT / 'public/img/articles'; img.mkdir(parents=True, exist_ok=True)
    (img / 'legacy-master-field-overflow-addr.svg').write_text(svg_hist(
        s, 'addrHist',
        f"会社{s['n']:,}件の所在地(都道府県から丁目番地等まで)の長さの分布。全角20文字を超えるのは{s['addrOver1']:,}件({s['addrOver1Pct']}%)、全角40文字を超えるのは{s['addrOver2']:,}件({s['addrOver2Pct']}%)",
        'lmfo-addr', 0, '会社の所在地の長さ(全角換算)と、住所1の上限', 'B社の住所1の上限(全角20文字)'), encoding='utf-8')
    foot = '国税庁法人番号公表サイト(国税庁)の全件データを加工して商いの帳場が作成。scripts/figs/master_field_overflow.py'
    P = lambda k: f"({s[k + 'Pct']}%)"
    rows = [('住所1(全角20)に入らない', s['addrOver1'], P('addrOver1'), '#678b2c'),
            ('うち20文字で切ると数字が割れる', s['splitBreaksNumber'], P('splitBreaksNumber'), '#b5cc8e'),
            ('住所1+住所2(全角40)に入らない', s['addrOver2'], P('addrOver2'), '#678b2c'),
            ('商号が全角40文字を超える', s['nameOver'], P('nameOver'), '#678b2c'),
            ('うち「(株)」に縮めれば入る', s['nameOverAbbrevFits'], f"({s['nameOver']:,}件中)", '#b5cc8e')]
    (img / 'legacy-master-field-overflow-bars.svg').write_text(svg_bars(
        rows, 'lmfo-bars', '会社の商号と所在地をB社の入力桁数に当てた件数の横棒グラフ。' + '、'.join(f'{a} {b:,}件' for a, b, _, _ in rows),
        f"会社{s['n']:,}件を入力桁数に当てて、あふれる件数", foot), encoding='utf-8')
