AppleScriptで表計算ソフトからJSONを生成する

## モチベーション
テストデータやクライアントサイドだけで持つJSONの生成はどうしていますか？自分は地道に手打ちして、JSONパーサーサイトでチェックして...という作業を行っていましたが、かなり面倒に感じました。また、仕様が変わりプロパティ(キー)を追加した時に全てのオブジェクトにそのプロパティを追加しなくてはならないという点でも良い方法とは言えません。そこで、Apple製表計算ソフトnumbersとAppleScriptを利用し、それらの問題の解決を試みました。

## 表から値を取得する
以下のような表を想定します。1行目にkeyとなる文字列を、その行にvalueが並んでいます。

![スクリーンショット 2023-12-19 10.08.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549093/05e4445f-51b6-292f-fd54-32b56a71f74e.png)

この表から以下のようなJSONファイルを生成するのがゴールとなります。

```json:recipe.json
[
{"title": "そら豆のパスタ", "foods": ["スパゲッティ", "そら豆", "パルミジャーノ", "オリーブ油", "バター"], "bookmark": true, "tags": ["パスタ", "春"], "id": 1},
{"title": "キャベツのパスタ", "foods": ["スパゲティ", "キャベツ", "アンチョビ", "にんにく", "赤唐辛子", "オリーブ油"], "bookmark": false, "tags": ["パスタ"], "id": 2}
]
```

## numbersファイルから辞書型を作るAppleScriptを用意する
valueが配列である場合には列がなくなるまで要素を取得し続ける必要があります。valueが要素数が一つの配列である場合と、配列の型でない場合の区別ができないため、valueの型に配列を使いたい場合には列を指定して特別な処理をする必要があります。

```AppleScript
tell application "Numbers"
	set jsonDictArray to {}
	open filePathOfNumbersFile
	-- アクティブなシートを取得
	set activeSheet to active sheet of document 1
	tell activeSheet
		set theTables to every table
		set myListStr to "[" & return
		repeat with theTable in theTables
			tell theTable
				set keyRecipe to value of cell 1 of row 1 of theTable
				set keyFoods to value of cell 2 of row 1 of theTable
				set keyBookmark to value of cell 3 of row 1 of theTable
				set keyTag to value of cell 4 of row 1 of theTable
				set keyID to value of cell 5 of row 1 of theTable
				set valueRecipe to value of cell 1 of row 2 of theTable
				set valueFoods to {}
				set rowIndex to 2 -- 開始行
				set columnNum to 2 -- 列番号
				repeat
					try
						set valueOfCell to value of cell columnNum of row rowIndex of theTable
						if valueOfCell is not missing value then
							set end of valueFoods to valueOfCell
							set rowIndex to rowIndex + 1
						else
							exit repeat
						end if
					on error
						exit repeat
					end try
				end repeat
				-- ブックマークを取得
				set valueBookmark to value of cell 3 of row 2 of theTable
				
				-- タグを取得
				set valueTag to {}
				set rowIndex to 2 -- 開始行
				set columnNum to 4 -- 列番号
				repeat
					try
						set valueOfCell to value of cell columnNum of row rowIndex of theTable
						if valueOfCell is not missing value then
							set end of valueTag to valueOfCell
							set rowIndex to rowIndex + 1
						else
							exit repeat
						end if
					on error
						exit repeat
					end try
				end repeat
				set valueID to value of cell 5 of row 2 of theTable
				set jsonDict to my createDictWith({{keyRecipe, valueRecipe}, {keyFoods, valueFoods}, {keyBookmark, valueBookmark}, {keyTag, valueTag}, {keyID, valueID}})
				set encoded to my encode(jsonDict)
				set myListStrLength to length of myListStr
				if myListStrLength = 2 then
					set myListStr to myListStr & encoded
				else
					set myListStr to myListStr & "," & return & encoded & return
				end if
			end tell
		end repeat
		set myListStr to myListStr & "]"
	end tell
end tell

```
取得した値を利用して辞書型に変換します。`"`をエスケープする必要があるのがポイントです。

## 辞書型を作るAppleScript

```AppleScript
on encode(value)
	set type to class of value
	if type = real then
		return (value as integer) as text
	else if type = boolean then
		return value as text
	else if type = text then
		return encodeString(value)
	else if type = list then
		return encodeList(value)
	else if type = script then
		return value's toJson()
	else
		error "Unknown type" & type
	end if
end encode

on encodeList(value_list)
	set out_list to {}
	repeat with value in value_list
		copy encode(value) to end of out_list
	end repeat
	return "[" & join(out_list, ", ") & "]"
end encodeList

on encodeString(value)
	return "\"" & value & "\""
end encodeString


on join(value_list, delimiter)
	set original_delimiter to AppleScript's text item delimiters
	set AppleScript's text item delimiters to delimiter
	set rv to value_list as text
	set AppleScript's text item delimiters to original_delimiter
	return rv
end join

on createDictWith(item_pairs)
	set item_list to {}
	script Dict
		on setkv(key, value)
			copy {key, value} to end of item_list
		end setkv
		
		on toJson()
			set item_strings to {}
			repeat with kv in item_list
				set key_str to encodeString(item 1 of kv)
				set value_str to encode(item 2 of kv)
				copy key_str & ": " & value_str to end of item_strings
			end repeat
			return "{" & join(item_strings, ", ") & "}"
		end toJson
	end script
	
	repeat with pair in item_pairs
		Dict's setkv(item 1 of pair, item 2 of pair)
	end repeat
	return Dict
end createDictWith
```

## 返された文字列をjsonファイルに書き出す

JSONが文字列として生成できたので、それを書き出します。`as «class utf8»`を使ってキャストしないと文字化けするので注意です。

```AppleScript
tell application "Finder"
	set aTextFile to open for access (書き出したいファイルへのパス & "ファイル名.json") with write permission
	set aEOF to get eof of aTextFile
	try
		write myListStr starting at 0 to aTextFile as «class utf8»
	on error aErrorText
		display dialog aErrorText
	end try
	close access aTextFile
end tell
```

## 終わりに
Object型に対応できていないのが心残りではありますが、抱えていた課題は解決できました。
AppleScriptはXcodeのテンプレートから削除されていたりと下火のようですが、便利なので使っていきたいです。

## 参考
JSONを作るAppleScriptを書くにあたり以下のリポジトリが大変参考になりました
https://github.com/mgax/applescript-json