CODEPOOL

趣味でやるプログラミング関連のMEMO置き場。

ビリヤード大会エントリー用のカスタムフォームを作ってみた

※2020/8/3 Demoサイトへのリンクを追加
※2020/8/8 btn-group-toggle クラスで作った radio button のインデントが解決

どうも、キヨタです。

しばらく更新が滞ってしまいましたがサボっていた訳ではありませんw
本業の繁忙期だったのと、インプットが多くてなかなか進まなかったのです。

今回はビリヤードの大会のエントリーフォームを実装してみました。
本当はかなり多岐にわたって試行錯誤したのですが真面目に書くと疲れるでこの記事では雰囲気だけ書くので留めておこうと思います。

ビリヤードで公式戦に出られる方はそのうちこのフォームに出会うかも。

検討背景

  • 私の趣味がビリヤードで業界的につながりがある
  • ビリヤード界隈は業界的にデジタル化が遅れている
  • 公式戦のエントリーがメールベースで運営が大変そう
  • 入力の抜け漏れないようにフォーム化すればええやん

検討成果

最終的には下図のようなものが出来上がりました。

f:id:yujikiyota:20200730003753p:plain
Entry Form 動作フロー

Demo : Sample大会 エントリーフォーム

  • Google Apps Script
    • フォーム制御
    • SpreadSheetへのデータ格納
    • メール自動応答
  • レスポンシブデザイン
    • Bootstrap (web)
    • mjml (mail)

どうしてもできなかった事

  • Bootstrapの btn-group-toggle クラスで作った radio button の左にどうしてもインデントが入ってしまう

[追記]

Bootstrapのform-checkクラスにpaddingが設定されていた事が問題だとの指摘があり、 下記の記述をstyleとして追加したら直りました。

        .form-check {
            padding: 0;
        }

参考文献

ほぼゼロから色々詰め込んだのでちょっと多すぎ

getbootstrap.com

www.htmq.com

relativelayout.hatenablog.com

tonari-it.com

qiita.com

siamcats.hateblo.jp

stackoverflow.com

qiita.com

qiita.com

qiita.com

tonari-it.com

www.aizulab.com

www.pre-practice.net

qiita.com

できる限りWindows 10のプリイン環境を使って見やすいコードの説明資料を作る

どうも、キヨタです。

基本このブログは仕事に関係ない趣味のプログラミング関係の話をダラダラと書くことを目的としていますが、 本日は業務上で懇切丁寧なコード説明資料を作りたくなったので環境を整えたという話を書こうと思います。

検討背景

  • 自分の職場はOSSフリーソフトなどの活用にあまり免疫がなく関係者の説得が面倒なので最小限に抑えたい
  • コーディングにうとい人を威嚇しないよう白背景のドキュメントを作りたい
    (背景が黒いだけで怖がる人がいる)
  • ダウンロードしてローカルでも見れるようにOfficeドキュメントを使いたい
    (はてブロとかのが書くのは楽なんですが機密情報とかDRMとか)

俺得要件

  • 最終ドキュメントはExcel (キャンバスがほぼ無限大)
  • ドキュメント上でのコード自体の読みやすさ
  • コーディング時はDark Theme
  • ドキュメントはLight Theme

準備したもの

azure.microsoft.com

言わずと知れた人気エディター。
Microsoft製なのでリッチテキスト形式でコピーができる!
Excelシンタックスハイライトしたコードを貼り付けられる!


  • Toggle Light/Dark Theme

marketplace.visualstudio.com

Visual Studio CodeのExtention。
Ctrl + Shift + Alt + T でLightテーマとDarkテーマが切り替えられる。

システムのカラースキームに合わせてトグルする設定があるらしいが、今回は不要なのでVisual Studio Codesettings.json に下記の1行を追記した。
これを追記しないとVisual Studio Code起動時に謎ポップアップが出てくるので割と鬱陶しかった。

    "window.autoDetectColorScheme": false,

その他設定

Web作ってたりするとどうしても日本語書く必要が出てきてしまうのでフォント選定がしたくなった。
Visual Studio Codeのデフォルトだと日本語はMS ゴシックになっているようで不快感がすごかったので、 Windows 10にデフォルトで入っている綺麗な等幅フォントである游ゴシックを採用。
英字フォントのデフォルトになっているConsolasは結構好みなので残した。

Visual Studio Codesettings.jsonに下記の1行を追記した。

    "editor.fontFamily": "Consolas, '游ゴシック Medium', 'Courier New', monospace",

できた!

f:id:yujikiyota:20200619174459p:plain
できる限りWindows 10のプリイン環境を使って見やすいコードの説明資料を作る

できたできた!
作業手順めっちゃ簡単ですごく満足感高いです。

  1. コーディングする (Dark Theme)
  2. Ctrl + Shift + Alt + T でLightテーマに切り替え
  3. Visual Studio Code上でソースコードをコピー
  4. Excelに貼り付けるとシンタックスハイライトされた白背景のコードが!

わーい!

若干気になる挙動

  • Ctrl + C でコピーしてExcelに貼り付けても何もペーストされない

何故かわかりませんが Ctrl + C でコピーするとExcelで貼り付けても何もペーストされませんでした。
なんか Visual Studio CodeVim Extention あたりが悪さしている気がしますが、 とりあえず下記の方法ではシンタックスハイライト含めてちゃんとペーストできました。

まぁ、すごい頻繁にやる作業ではないのでこれぐらいは許容範囲内ですかね。 個人的には十分満足できました。

よかったよかった。

続・カメラからのデータ取り込みコマンドを作ってみた

どうも、キヨタです。

先日作成したカメラからのデータ取り込みコマンドに早速不満が出てきたので書き直してみました。

元記事はこちらになります。

codepool.hatenablog.com

データコピーで色々使ってみると撮映日がファイル作成日と異なるケースがあり、コピー先の仕分けが期待と異なる結果となってしまっていました。

そこで、今回は静止画ファイルに限りExifから撮映日を取得してきて保存先の仕分けをするように実装変更しました。

仕様おさらい

f:id:yujikiyota:20200525010427p:plain
カメラからのデータ取り込みコマンドイメージ

  • 取り込み元メディアでは指定ディレクトリ以下のサブディレクトリまで探索し、拡張子でマッチして取り込むファイルを決める
  • 取り込み先ではyyyy/yyyy-mm/yyyy-mm-ddのような撮影日の年月日ベースの階層構造でディレクトリを分けて格納する
  • オプションで静止画のみ/動画のみの取り込みにも対応できるようにする
  • ファイル作成日などのメタデータは保持してコピーしてくる、元ファイルは消さない
  • 取り込み先に同一名のファイルがある場合にはサイズ比較で同一性を確かめてログに吐く

今回の変更点

  • 静止画ファイルはExifから撮映日を取得する Pillow
  • コード冒頭の拡張子リストの大文字/小文字並記回避
  • 要素機能の関数化 def
  • for文制御のフィルタ使用 .filter() lambda
  • コマンドライン引数処理の分離 main()

ソースコード

#!/usr/bin/env python3

import sys
import shutil
import datetime
from pathlib import Path
from PIL import Image
from PIL.ExifTags import TAGS

# Define directory format
dir_format = '%Y/%Y-%m/%Y-%m-%d'

# Define copying file extentions
image_ext_tuple = ('jpg', 'jpeg')
movie_ext_tuple = ('mp4', 'm2ts')


def get_exif_date(img):
    exif = img._getexif()
    try:
        for id, val in exif.items():
            tg = TAGS.get(id, id)
            if tg == "DateTimeOriginal":
                # Convert 'yyyy:mm:dd hh:mm:ss' -> 'yyyy-mm-dd'
                date_val = val.split(' ')
                return date_val[0].replace(':', '-')

    # If there is no exif tags
    except AttributeError:
        return 'NON'

    # If there is no date exif value
    return 'NON'


def copy_media_classify_by_date(path_obj_src, path_obj_dst, dir_format, ext_tuple):
    # Get file list
    src_path_list = list(path_obj_src.glob('**/*'))

    # Copy files to YYYY/MM/DD directory
    for src_path in list(filter(lambda path: path.suffix.replace('.', '').lower() in ext_tuple, src_path_list)):

        # Try to get image taken date
        try:
            img = Image.open(src_path, mode="r")
            taken_date = get_exif_date(img)
        except Exception:
            taken_date = 'NON'

        # Get create date
        create_date = datetime.datetime.fromtimestamp(
            src_path.stat().st_ctime).strftime('%Y-%m-%d')

        # Convert directory path string
        if taken_date == 'NON':
            date_dir = datetime.datetime.fromisoformat(
                create_date).strftime(dir_format)
        else:
            date_dir = datetime.datetime.fromisoformat(
                taken_date).strftime(dir_format)

        # Make YYYY/MM/DD directory if it does not exist
        path_obj_dst_date = path_obj_dst / date_dir
        if not path_obj_dst_date.exists():
            path_obj_dst_date.mkdir(parents=True)
            print('\n  Make directory : {dir}\n'.format(
                dir=str(path_obj_dst_date)))

        # Skip copying if target file is already exist
        file_name = src_path.name
        dst_path = path_obj_dst_date / file_name

        if dst_path.exists():
            # Judge as 'same file' if target file size is same as source
            if dst_path.stat().st_size == src_path.stat().st_size:
                status = '...Skipped : Target file is already exist.'
            else:
                status = '...Skipped : Target file is already exist. (different size)'

        # Copy file with meta-data (copy2)
        else:
            shutil.copy2(src_path, dst_path)
            status = '...Copied to ' + str(path_obj_dst_date)

        # Print terminal message
        if taken_date == 'NON':
            print('  {filename} [{ctime}] {stat}'.format(
                filename=file_name,
                ctime=create_date,
                stat=status))
        else:
            print('  {filename} [{ttime}] {stat}'.format(
                filename=file_name,
                ttime=taken_date,
                stat=status))


def main():
    # Process arguments
    args = sys.argv

    usage = '  Usage : ' + \
        args[0].replace('./', '') + ' [-h | -i | -m] source_dir target_dir'
    usage += """

        -h      Show usage.

        -i      Copying image files only.

        -m      Copying movie files only.
    """

    for arg in args:
        if arg == args[0]:
            pass
        elif arg == '-h' or arg == '--help':
            print('\n' + usage)
        elif arg == '-i':
            ext_tuple = image_ext_tuple
        elif arg == '-m':
            ext_tuple = movie_ext_tuple
        elif 'path_obj_src' not in locals():
            path_obj_src = Path(arg)
        elif 'path_obj_dst' not in locals():
            path_obj_dst = Path(arg)

    if 'ext_tuple' not in locals():
        ext_tuple = image_ext_tuple + movie_ext_tuple

    # Check input exeptions
    if 'path_obj_dst' not in locals():
        print('\n  Error : Too few arguments.')
        print(usage)
        sys.exit(1)

    # Print header
    print('-' * 50 + '\n')
    print('  ' + str(args[0]).replace('./', '') + '\n')
    print('  Source dir : \n    ' + str(path_obj_src))
    print('  Destination root dir : \n    ' + str(path_obj_dst))
    print('  File type : \n    ' + str(ext_tuple) + '\n')
    print('-' * 50 + '\n')

    copy_media_classify_by_date(
        path_obj_src, path_obj_dst, dir_format, ext_tuple)


if __name__ == '__main__':
    main()

結果と所感

  • ちゃんとExifから撮映日を取得することで正しい動作が得られた
  • 要素機能をくくり出し→関数化はもっと修行が必要
  • filterとかlambda式とか便利やん

参考文献

glorificatio.org

it-engineer-lab.com

docs.python.org

2次元テーブルデータの内挿補間関数を書いてみた

どうも、キヨタです。

今まで謎に未経験でしたが、血迷ってExcel VBAデビューしてみました。

2次元テーブルデータから中間の値を内挿補間して求める作業を山ほどやらなければならなくなったのですが、MATCHとかVLOOKUPとかで管理するのがもう面倒になったので関数化してみた次第です。

結果的には大変快適でした。

要求仕様

f:id:yujikiyota:20200603004817p:plain
Fig.1 Excel上でのTableデータ配置イメージ

想定しているデータはFig.1に示したような2次元の数値データで、x_rangeを定義域とするx_valy_rangeを定義域とするy_valで示されたIndexに相当するデータをdata_range内のデータから内挿補間して生成する関数を作ります。

作っている途中でx_val y_valが定義域から外れるとよくわからない謎の数値を返す事が分かったので、定義域外の入力に対しては#N/Aを返すことにしました。

f:id:yujikiyota:20200603004054p:plain
Fig.2 2次元テーブルデータの内挿補間

内挿補間の方法は至ってシンプルで、VBAからMatch関数で引き当てたd11 d12 d21 d22を用いて下記の計算をしています。


\begin{aligned}
d_{m1} &= d_{11} + (d_{21}-d_{11})\times\frac{x_{val}-x_1}{x_2-x_1} \\
d_{m2} &= d_{12} + (d_{22}-d_{12})\times\frac{x_{val}-x_1}{x_2-x_1} \\
d_{mm} &= d_{m1} + (d_{m2}-d_{m1})\times\frac{y_{val}-y_1}{y_2-y_1}  
\end{aligned}

ソースコード

Function InterLin2D(x_val As Double, x_range As Range, y_val As Double, y_range As Range, data_range As Range)

    Dim x_index, y_index As Long
    Dim x1, x2, y1, y2, x_min, x_max, y_min, y_max, d11, d12, d21, d22, dm1, dm2, dmm As Double
    
    ' Get min/max value of x/y axis
    x_min = WorksheetFunction.Min(x_range)
    x_max = WorksheetFunction.Max(x_range)
    y_min = WorksheetFunction.Min(y_range)
    y_max = WorksheetFunction.Max(y_range)
    
    ' Get smaller value index
    x_index = WorksheetFunction.Match(x_val, x_range, 1)
    y_index = WorksheetFunction.Match(y_val, y_range, 1)
    
    ' Get reference axis value
    x1 = x_range(x_index)
    x2 = x_range(x_index + 1)
    y1 = y_range(y_index)
    y2 = y_range(y_index + 1)
    
    ' Get reference data value
    d11 = data_range(y_index, x_index)
    d12 = data_range(y_index + 1, x_index)
    d21 = data_range(y_index, x_index + 1)
    d22 = data_range(y_index + 1, x_index + 1)
    
    ' Calc interpolated data value for x-direction
    dm1 = d11 + (d21 - d11) * (x_val - x1) / (x2 - x1)
    dm2 = d12 + (d22 - d12) * (x_val - x1) / (x2 - x1)

    ' Calc 2D interpolated data value
    dmm = dm1 + (dm2 - dm1) * (y_val - y1) / (y2 - y1)
    
    ' If x_val/y_val is out of function domain, return #N/A
    If x_min <= x_val And x_val < x_max And y_min <= y_val And y_val < y_max Then
        InterLin2D = dmm
    Else
        InterLin2D = CVErr(xlErrNA)
    End If
    
End Function

所感

  • 血迷ってExcel VBAデビューしてみたらMATCHとかINDEXとかのセル直書きから開放されて結構快適だった
  • なんかreturn文とかが無さそうだったので引数の定義域外判定がソースコード後半にきてしまって気持ち悪かったので良い方法がないか調べたい
  • ExcelのコードエディタをVS Codeみたくもう少し格好よくしても良いと思いますMicrosoftさん
  • どうやらC#とかでも書けるらしいので一度試してみたい。

カメラからのデータ取り込みコマンドを作ってみた

どうも、キヨタです。

プログラミングちゃんと勉強してこなかったので趣味がてら色々作ってみようと思ってPython始めました。 Pythonは初心者なのでお作法とかあまり分からず、とりあえずググりながら書いてみています。

今年の3月に人生初のMacを購入し、子供が寝静まった深夜にニヤニヤしながらカタカタしているのですが、Win機のころ使用していたカメラ画像の取り込みソフトがMacでサポートされておらず写真整理が滞っていました。 そこで記念すべき第1段はカメラからの動画/静止画取り込みを想定した俺得コマンドを作る事にしました。

仕様

f:id:yujikiyota:20200525010427p:plain
カメラからのデータ取り込みコマンドイメージ

  • 取り込み元メディアでは指定ディレクトリ以下のサブディレクトリまで探索し、拡張子でマッチして取り込むファイルを決める
  • 取り込み先ではyyyy/yyyy-mm/yyyy-mm-ddのような撮影日の年月日ベースの階層構造でディレクトリを分けて格納する
  • オプションで静止画のみ/動画のみの取り込みにも対応できるようにする
  • ファイル作成日などのメタデータは保持してコピーしてくる、元ファイルは消さない
  • 取り込み先に同一名のファイルがある場合にはサイズ比較で同一性を確かめてログに吐く

試してみたこと

  • コマンドライン引数の処理 (sys.argv)
  • ファイルパス操作 (pathlib)
  • ファイルのメタ情報の確認 (.stat())
  • 文字列フォーマットの練習 (.format)

使い方と実行結果

使い方はシンプル。
コピー元とコピー先の親ディレクトリをコマンドライン引数で指定します。
-i オプションをつけると静止画のみ、-mをつけると動画のみをコピーします。

> python camera_import.py 

  Error : Too few arguments.
  Usage : camera_import.py [-h | -i | -m] source_dir target_dir

    -h      Show usage.

    -i      Copying image files only.

    -m      Copying movie files only.

> python camera_import.py /Volumes/Source /Volumes/Destination
--------------------------------------------------

  camera_import.py

  Source dir : /Volumes/Source
  Target dir : /Volumes/Destination
  File type  : ('JPG', 'jpg', 'mp4', 'MP4', 'm2ts', 'M2TS')

--------------------------------------------------

   Make directory : /Volumes/Destination/2020/2020-05/2020-05-20

  DSC01052.JPG [2020/05/20] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-20
  DSC01053.JPG [2020/05/20] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-20
  DSC01054.JPG [2020/05/20] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-20
  DSC01055.JPG [2020/05/20] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-20
  DSC01056.JPG [2020/05/20] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-20
  DSC01057.JPG [2020/05/20] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-20
  DSC01058.JPG [2020/05/20] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-20
  DSC01059.JPG [2020/05/20] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-20
  
  Make directory : /Volumes/Destination/2020/2020-05/2020-05-21

  DSC01071.JPG [2020/05/21] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-21
  DSC01072.JPG [2020/05/21] ...Copied to /Volumes/Destination/2020/2020-05/2020-05-21

...

ソースコード

#!/usr/bin/env python3
#
#   camera_import.py
#
#   Author  : Yuji.Kiyota@gmail.com
#   Create  : 2020/05/22
#

from pathlib import Path
import sys
import shutil
import datetime

# Define directory format
dir_format = '%Y/%Y-%m/%Y-%m-%d'

# Define copying file extentions (case sensitive)
image_ext_tuple = ( 'JPG', 'jpg' )
movie_ext_tuple = ( 'mp4', 'MP4', 'm2ts', 'M2TS' )

# Process arguments
args = sys.argv

usage = '  Usage : ' + args[0].replace( './', '' ) + ' [-h | -i | -m] source_dir target_dir'
usage += """

    -h      Show usage.

    -i      Copying image files only.

    -m      Copying movie files only.
"""

for arg in args :
    if arg == args[0] :
        pass
    elif arg == '-h' or arg == '--help' :
        print( '\n' + usage )
    elif arg == '-i' :
        ext_tuple = image_ext_tuple
    elif arg == '-m' :
        ext_tuple = movie_ext_tuple
    elif 'path_obj_src' not in locals() :
        path_obj_src = Path( arg )
    elif 'path_obj_dst' not in locals() :
        path_obj_dst = Path( arg )

if 'ext_tuple' not in locals() :
    ext_tuple = image_ext_tuple + movie_ext_tuple

# Check input exeptions
if 'path_obj_dst' not in locals() :
    print( '\n  Error : Too few arguments.' )
    print( usage )
    sys.exit(1)

# Print header
print( '-' * 50 + '\n' )
print( '  ' + str( args[0] ).replace( './', '' ) + '\n' )
print( '  Source dir : \n    ' + str( path_obj_src ) )
print( '  Destination root dir : \n    ' + str( path_obj_dst ) )
print( '  File type : \n    ' + str( ext_tuple ) + '\n' )
print( '-' * 50 + '\n' )

# Get file list
src_file_path_list = []
for file_ext in ext_tuple :
    src_file_path_list += list( path_obj_src.glob( '**/*.' + file_ext ) )

# Copy files to YYYY/MM/DD directory
for src_file_path in src_file_path_list :

    # Get file create time as YYYY/MM/DD format
    file_name = src_file_path.name
    create_date = datetime.datetime.fromtimestamp( src_file_path.stat().st_ctime ).strftime( '%Y/%m/%d' )
    date_dir = datetime.datetime.fromtimestamp( src_file_path.stat().st_ctime ).strftime( dir_format )
    
    # Make YYYY/MM/DD directory if it does not exist
    path_obj_dst_date = path_obj_dst / date_dir
    if not path_obj_dst_date.exists() :
        path_obj_dst_date.mkdir(parents=True)
        print( '\n  Make directory : {dir}\n'.format( dir = str( path_obj_dst_date ) ) )

    # Skip copying if target file is already exist
    dst_file_path = path_obj_dst_date / file_name
    if dst_file_path.exists() :

        # Judge as 'same file' if target file size is same as source
        if dst_file_path.stat().st_size == src_file_path.stat().st_size :
            status = '...Skipped : Target file is already exist.' 
        else :
            status = '...Skipped : Target file is already exist. (different size)'

    # Copy file with meta-data (copy2)
    else :
        shutil.copy2( src_file_path, dst_file_path )
        status = '...Copied to ' + str( path_obj_dst_date )

    # Print terminal message
    print( '  {filename} [{ctime}] {stat}'.format( \
        filename = file_name, \
        ctime = create_date, \
        stat = status ) )

まとめと残課題

  • とりあえず思った通りの取り込み動作の実装はできたので良かった
  • shutil.copy2()がコケると処理が止まってしまうので例外処理とか必要っぽい
  • 内部処理を関数化?オブジェクト化?して、直接呼び出しの時のみ引数処理をするような書き方がもっとPythonらしい気がする
  • GooglePhotoにUploadするコマンドも作りたい

今後もマイペースに続けていきたいと思います。

Macの設定MEMO

設定関連で参考にした記事の覚書。 随時更新。

Mac設定

qiita.com

インストールしたソフト

Terminal

qiita.com

  • Font: Menlo

VS Code

qiita.com

Python

koma-log.com

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com