echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【python】pythonでExcelを操作する その2【brainf*ck】

前回Pythonで簡単なExcel操作について書きました。
今回はもうちょっと踏み込んだものを、と考えた結果「brainf*ck」のインタプリタPythonで作る事にしました。

ちなみに「brainf*ck」とは...

Brainfuck(ブレインファック)は難解プログラミング言語のひとつ。なお名称に卑語が含まれるため、Brainf*ckなどと表記されることがある。
(Wikipediaより)

だそうです。

ちなみに、下記命令文字列は「Hello,Python」と出力するものなのですが...なんのことやら、さっぱりですね。

 +++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.>-.<<++++++++.>>>+++++++++++[>+++++++++++<-]>.-----.<++++[>---<-]>.<<<.-.

なお参考としては、先述のWikipedia(https://ja.wikipedia.org/wiki/Brainfuck)、

また実際の動きを見たい場合、以前ネットニュースでも話題になった、t-kihiraさんの動画が大変分かりやすくてよいと思います。
(こちらはjavascriptで作成してます。てかHTMLやDOM操作など全込みで1時間は凄すぎ!)
nicotter.net

どこをExcelと連携するか

ですが、brainf*ckには

  • 命令文字列(><+-.,[])の読み込み
  • メモリへの書き込み

があるので、前者をExcelシートから読み込み、後者をExcelシートに書き込もうと思います。

共通変数は?

共通変数(≒クラス変数)ですが、先述のWikipediaに、brainf*ckで管理が必要そうなものとして

  • 現在のポインタ
  • メモリの値(アドレスごと)

がありますので、これを共通変数にすればよいかと。

また、「メモリのアドレスの個数」あたりも共通変数にしとくと便利そうです。

あ、肝心のExcel関連(ブック・シート)も共通変数にしたほうが便利そうですね。

で、まあこんな感じかと。

import sys
sys.path.append([openpyxlの格納パス])  ##デフォルトで読み込み可能なら不要

import openpyxl as pyxl
from openpyxl import load_workbook

MEMORY = []  ##メモリの値(アドレス毎)  
POINTER = 0  ##現在のポインタ
MEMORY_LENGTH = 30  ##メモリのアドレスの個数
RESULT_STRING=""

##対象のブックとシート
WORK_BOOK = load_workbook('brainf_ck.xlsx')
WORK_SHEET = WORK_BOOK.get_sheet_by_name('Sheet1')


実際の読み書き処理

ですが、これは例えば

  • B1セルは命令文字列
  • B4~K6セルはメモリの値

というように決め打ちすれば、比較的楽ではないかと。

またメモリ書き込みは何回も呼び出すので、関数にしておきましょう。

で、コードとしてはこんな感じかな?
(てかpythonって、文字列の切り出しが'変数名[添字]'でいいんですね。
substring()とかいらないので、便利です。)

## 命令文字列の読み込み
command_string = str(WORK_SHEET['B1'].value)
## 命令文字列の書き込み関数
## valueは書き込む値(数値)
def writeMemoryToSheet(value):

    #global POINTER
    memory_string_length = len("000" + str(value))
    memory_string = ("000" + str(value))[memory_string_length -3:]

    row = 0
    col = (POINTER % 10) + 2

    if(POINTER <= 9):
        row = 4
    elif(POINTER >= 10 and POINTER <= 19):
        row = 5
    else:
        row = 6

    WORK_SHEET.cell(row=row, column=col).value = memory_string

命令文本体

で、肝心の命令文ですが、Wikipediaによると、

  • >: ポインタをインクリメントする。ポインタをptrとすると、C言語の「ptr++;」に相当する。
  • <: ポインタをデクリメントする。C言語の「ptr--;」に相当。
  • +: ポインタが指す値をインクリメントする。C言語の「(*ptr)++;」に相当。
  • -: ポインタが指す値をデクリメントする。C言語の「(*ptr)--;」に相当。
  • .: ポインタが指す値を出力に書き出す。C言語の「putchar(*ptr);」に相当。
  • ,: 入力から1バイト読み込んで、ポインタが指す先に代入する。C言語の「*ptr=getchar();」に相当。
  • [: ポインタが指す値が0なら、対応する ] の直後にジャンプする。C言語の「while(*ptr){」に相当。
  • ]: ポインタが指す値が0でないなら、対応する [ (の直後[1])にジャンプする。C言語の「}」に相当[2]。

ということですが、下2つ以外は、特に問題ないかと思います。
他の「brainf*ck作ったよ!」というサイトなどでも説明されていますが、

  • POINTERの値を加減する
  • MEMORY[POINTER]の値を加減する...

といった感じですね。

MEMORY[POINTER]の値を加減した場合、先程の関数でExcelにも値を書きます。
また、コンソールからの入力にも対応してあげなくてはなりません(=input()関数)
※chr(value)関数は、value文字コードを文字に変換する関数。
 逆にord(value)関数は、valueの文字を文字コードに変換する関数。

てかPythonって、switch case構文ないんですね...

   i = 0

    while i < len(command_string):

        char = command_string[i]

        if(char == ">"):
            POINTER += 1
            if(POINTER >= MEMORY_LENGTH):
                print("エラー:不正な\'>\'があります。")
                return
        elif (char == "<"):
            POINTER -= 1
            if(POINTER < 0):
                print("エラー:不正な\'<\'があります。")
                return
        elif(char == "+"):
            MEMORY[POINTER] += 1
            writeMemoryToSheet(MEMORY[POINTER])
        elif(char == "-"):
            MEMORY[POINTER] -=1
            writeMemoryToSheet(MEMORY[POINTER])
        elif(char == "."):
            RESULT_STRING += chr(MEMORY[POINTER])
        elif(char == ","):
            while True:
                input_char = input("半角英数1文字を入力してください。")
                if(len(input_char) != 1):
                    print("エラー:1文字ではありません。")
                else:
                    char_code = ord(input_char)

                    if(char_code < 0 or 127 < char_code):
                        print("エラー:半角文字ではありません。")
                    else:
                        MEMORY[POINTER] = char_code
                        writeMemoryToSheet(MEMORY[POINTER])
                        break
        i += 1


問題の下二つ

ですが、これは「'['や']'があったら、対応する']'や'['を見つける」処理が必要です。
この処理は関数にしてしまい、「対応する'['や']'の位置」は、リスト(他言語でいう配列)で管理すると楽です。

てな訳で、ソースはこちら。

## idxはcommand_string内での'['の位置
## target_stringはcommand_string。
## KAKKOは対応する位置を管理するlist(共通関数にする)

def checkKakkoRange(idx, target_string):

    for i in range(idx + 1, len(target_string)):

        kakko_count = 0
        check_char = target_string[i]

        if(check_char == "["):
            ## 2重以上の'['があったら、kakko_countを+1する
            kakko_count += 1
        elif(check_char == "]"):

            if(kakko_count == 0):
                ##kakko_countが0の場合の']'は、対応する']'である。
                KAKKO[idx] = i
                KAKKO[i] = idx
                break
            else:
                ##そうじゃない場合の']'は、対応する']'ではないので、kakko_countを-1する。
                kakko_count -= 1

で、先ほどの「命令文本体」のところに、こんな分岐処理を追加すればよいかと。

        if(char == ">")
            ## 中略
        elif(char == "["):
            ## 初回のみ、checkKakkoRange()を呼び出す。
            if(KAKKO[i] == -1):
                checkKakkoRange(i, command_string)
            
            if(MEMORY[POINTER] == 0):
                ## ちゃんと対応する']'があるか?
                if(KAKKO[KAKKO[i]] == i):
                    i = KAKKO[i] + 1
                    continue
                else:
                    print("エラー:\'[\'\']\' の数が一致しません。")
                    return
        elif(char == "]"):
            ## ']'の場合、checkKakkoRange()は呼び出し不要。(対応する'['の時点で呼び出されてるはずなので)
            if(MEMORY[POINTER] != 0):
                if(KAKKO[KAKKO[i]] == i):
                    i = KAKKO[i] + 1
                    continue
                else:
                    print("エラー:\'[\'\']\' の数が一致しません。")
                    return


ソース全体

import sys
sys.path.append("C:\\Program Files\\Anaconda3\\Lib\\site-packages\\")

import openpyxl as pyxl
from openpyxl import load_workbook

#print(pyxl.__file__)

MEMORY = []
KAKKO = []
POINTER = 0
MEMORY_LENGTH = 30
RESULT_STRING=""

WORK_BOOK = load_workbook('brainf_ck.xlsx')
WORK_SHEET = WORK_BOOK.get_sheet_by_name('Sheet1')

def init():
    for i in range(MEMORY_LENGTH):

        global POINTER

        POINTER = i
        MEMORY.append(0)
        ##ワークシートに書き出し
        writeMemoryToSheet(MEMORY[i])

    POINTER = 0

def interpreter():

    global RESULT_STRING
    global POINTER
    command_string = str(WORK_SHEET['B1'].value)

    if(command_string.count('[') != command_string.count(']')):
        print("エラー:\'[\'\']\' の数が一致しません。")
        return

    for j in range(len(command_string)):
        KAKKO.append(-1)

    i = 0
    if(command_string[0] == "'"):
        i = 1

    while i < len(command_string):

        char = command_string[i]

        if(char == ">"):
            POINTER += 1
            if(POINTER >= MEMORY_LENGTH):
                print("エラー:不正な\'>\'があります。")
                return
        elif (char == "<"):
            POINTER -= 1
            if(POINTER < 0):
                print("エラー:不正な\'<\'があります。")
                return
        elif(char == "+"):
            MEMORY[POINTER] += 1
            writeMemoryToSheet(MEMORY[POINTER])
        elif(char == "-"):
            MEMORY[POINTER] -=1
            writeMemoryToSheet(MEMORY[POINTER])
        elif(char == "."):
            RESULT_STRING += chr(MEMORY[POINTER])
        elif(char == "["):
            if(KAKKO[i] == -1):
                checkKakkoRange(i, command_string)

            if(MEMORY[POINTER] == 0):
                if(KAKKO[KAKKO[i]] == i):
                    i = KAKKO[i] + 1
                    continue
                else:
                    print("エラー:\'[\'\']\' の数が一致しません。")
                    return
        elif(char == "]"):
            if(MEMORY[POINTER] != 0):
                if(KAKKO[KAKKO[i]] == i):
                    i = KAKKO[i] + 1
                    continue
                else:
                    print("エラー:\'[\'\']\' の数が一致しません。")
                    return
        elif(char == ","):
            while True:
                input_char = input("半角英数1文字を入力してください。")
                if(len(input_char) != 1):
                    print("エラー:1文字ではありません。")
                else:
                    char_code = ord(input_char)

                    if(char_code < 0 or 127 < char_code):
                        print("エラー:半角文字ではありません。")
                    else:
                        MEMORY[POINTER] = char_code
                        writeMemoryToSheet(MEMORY[POINTER])
                        break
        i += 1

def checkKakkoRange(idx, target_string):

    for i in range(idx + 1, len(target_string)):

        kakko_count = 0
        check_char = target_string[i]

        if(check_char == "["):
            kakko_count += 1
        elif(check_char == "]"):

            if(kakko_count == 0):
                KAKKO[idx] = i
                KAKKO[i] = idx
                break
            else:
                kakko_count -= 1


def writeMemoryToSheet(value):

    #global POINTER
    memory_string_length = len("000" + str(value))
    memory_string = ("000" + str(value))[memory_string_length -3:]

    row = 0
    col = (POINTER % 10) + 2

    if(POINTER <= 9):
        row = 4
    elif(POINTER >= 10 and POINTER <= 19):
        row = 5
    else:
        row = 6

    WORK_SHEET.cell(row=row, column=col).value = memory_string

##ここからメイン処理
init()
interpreter()

WORK_BOOK.save('brainf_ck.xlsx')

print("文字列:" + RESULT_STRING)
print("\n" + "終了")


ちなみに、実際に実行させてみた動画がこちら。

いや、やはりどんなものであれ、まずは1個何か作ってみると、(Pythonに限らず)その言語の理解がより深まりますね。

てかbrainf*ck、「難解言語」とか「ク*言語」とか言われるけど、文字列、メモリ、ポインタの考え方とか、むしろ初学者とかの教育にはかなり向いてるんじゃないの?とか思ったり...

ただ、もっとポインタやメモリの動きがリアルタイムに分かるといいなあ...
(本当はExcelを表示させながらやる予定だったが、起動してたら書き込み時にエラーになるので、断念...

てか、当然といえば当然ですよね...)