サポンテ 勉強ノート

サポンテの勉強ノート・読書メモなどを晒します。

if〜else 文の周辺のコメントの書き方と注意事項

はじめに

 if〜else 構造と、付随するコメントの書き方で、以下のようなコードを見ました。

/* --- List 1 --- */

// 20 以上の場合
if (age >= 20)
{
    console.log("20以上です");
}
// 未成年の場合
else
{
    console.log("未成年です");
}

 上記 List 1 について「コメント内容」「位置」「書き方」について思うところを書きます。

続きを読む

TSV(Excel または他の表計算ソフト)から XML コメント上の表に変換するコードジェネレータ

はじめに

 C#VB.NET で使われる XML コメントには「表」を書くことができます(まあ書いている人は見たことありませんが)。正確には「table タイプの list」でしょうか。書き方を調べてみると、噴飯ものの煩わしさです。

 先日、味をしめた方法で自動化します。

続きを読む

Excel のワークシート関数で正規表現を使う【Mac版】

はじめに

 Excel のワークシート関数には正規表現が使えません。ですが、ユーザー定義関数を追加することで使うことができます。

 Windows 版には、すでにいくつかの作例があります。

 しかし上記の作例は Windows 固有のライブラリを使用するため Mac では利用できません。本記事では Mac 版を実装してみます。

 開発には Excel:mac 2011 を使用しましたが、2016 以降では別途対応が必要かもしれません。以下を参考にしてください。

実装方法

 VBA からシェルコマンドで PHP を呼び出して、PHP正規表現処理を利用しています。

 Mac は バックスラッシュ(Chr(&h5C))と円記号(Chr(%h80))が明確に分かれているのでハマりました。

 シェルの呼び出し(結果の受け取り)は以下のコードを参考にさせていただきました。

excel - VBA Shell function in Office 2011 for Mac - Stack Overflow

インストールの仕方

 まず「ユーザー定義関数とはなにか」というところから知りたい場合は > こちらをご覧ください。

 後述のソースコードを、どのように Excel に設定してユーザー定義関数として使えるようにするかについては > こちらをご覧ください。

使い方

関数 REGEXP_MATCH()

関数の記述例
=REGEXP_MATCH(A1, "([A-Z])\w+")
関数の定義
=REGEXP_MATCH(引数1, 引数2, [[引数3], 引数4])

引数

引数1:subject

 正規表現検索を行う対象の文字列を入力します。参照先のセルアドレスでも構いません。

引数2:pattern

 正規表現パターンの文字列を指定します。

引数3:subMatchIndex

 オプションです。指定しない場合、最初にマッチした文字列を返します。

 2 を指定すれば二番目にマッチした文字列を返します。

引数4:isIgnoreCase

 オプションです。既定は FALSE です。

 TRUE を指定すると、大文字小文字の違いを無視した検索を行います。

返値

 マッチした文字列を返します。

ソースコード

 開発環境は以下の通りです。

Option Explicit

Const REGEXP_MATCH_DEBUG_MODE As Boolean = False

Private Declare Function popen Lib "libc.dylib" (ByVal command As String, ByVal mode As String) As Long
Private Declare Function pclose Lib "libc.dylib" (ByVal file As Long) As Long
Private Declare Function fread Lib "libc.dylib" (ByVal outStr As String, ByVal size As Long, ByVal items As Long, ByVal stream As Long) As Long
Private Declare Function feof Lib "libc.dylib" (ByVal file As Long) As Long
Private Declare Function regex_match Lib "libc++.dylib" (ByVal basic_string As String, ByVal basic_regex As String, ByVal match_flag_type As Integer)

Public Sub RegisterREGEXP_MATCH()
'    Application.MacroOptions Macro:="REGEXP_MATCH", _
'        Description:="正規表現検索を行い、マッチした文字列を返します。", _
'        Category:="文字列操作", _
'        HelpFile:=""
    Application.Volatile True
    Application.MacroOptions _
        Macro:="REGEXP_MATCH", _
        Description:="正規表現検索を行い、マッチした文字列を返します。", _
        Category:="文字列操作"
End Sub

Public Function REGEXP_MATCH(subject As String, _
                             pattern As String, _
                    Optional subMatchIndex As Integer = -1, _
                    Optional isIgnoreCase As Boolean = False) As Variant

    Dim optionChars As String
    Dim cmd As String
    Dim result As String
    Dim exitCode As Long

    If isIgnoreCase Then
        optionChars = "i"
    End If

    If subMatchIndex < 0 Then subMatchIndex = 1

    cmd = CreateCommandString(subject, pattern, subMatchIndex - 1, optionChars)
    result = execShell(cmd, exitCode) 'ByRef exitCode

    If exitCode <> 0 Then
        REGEXP_MATCH = CVErr(xlErrNA)
        Debug.Print result
    Else
        REGEXP_MATCH = result
    End If
End Function

Private Function CreateCommandString(ByVal subject As String, ByVal pattern As String, ByVal idx As String, ByVal optionChars As String) As String
    Dim BS As String
    Dim QQ As String
    Dim SQ As String
    
    Dim s As String
    Dim p As String
    s = SubjectEscape(subject)
    p = RegexPatternEscape(pattern)
    
    BS = Chr(&H5C)
    QQ = """"
    SQ = "'"
    
    Dim cmd As String
    cmd = "php -r " & SQ & _
        "$m=" & QQ & QQ & ";" & _
        "try{" & _
        "if(false!==preg_match_all(" & QQ & "/" & p & "/" & optionChars & QQ & ", " & _
        QQ & s & QQ & ",$m,0,0)){" & _
        "echo $m[0][" & idx & "];" & _
        "}" & _
        "}catch(Exception $e){" & _
        "var_dump($e);" & _
        "exit(1);" & _
        "}" & _
        "exit(0);" & _
        SQ
    
    If REGEXP_MATCH_DEBUG_MODE Then
        cmd = cmd & " 2>&1"
        Debug.Print cmd
    End If
    
    CreateCommandString = cmd
End Function

Private Function SubjectEscape(ByVal src As String) As String
    Dim BS As String
    Dim QQ As String
    Dim SQ As String
    BS = Chr(&H5C)
    QQ = """"
    SQ = "'"
    
    Dim ret As String
    ret = src
    ret = Replace(ret, Chr(&H80), Chr(&H5C))
    ret = Replace(ret, QQ, BS & QQ)
    ret = Replace(ret, SQ, BS & SQ)
    ret = Replace(ret, vbLf, BS & "n")
    SubjectEscape = ret
End Function

Private Function RegexPatternEscape(ByVal src As String) As String
    Dim BS As String
    Dim QQ As String
    Dim SQ As String
    BS = Chr(&H5C)
    QQ = """"
    SQ = "'"
    
    Dim ret As String
    ret = src
    ret = Replace(ret, Chr(&H80), Chr(&H5C))
    ret = Replace(ret, QQ, BS & QQ)
    ret = Replace(ret, SQ, BS & SQ)
    ret = Replace(ret, vbLf, BS & "n")
    RegexPatternEscape = ret
End Function

Private Function execShell(command As String, Optional ByRef exitCode As Long) As String
    Dim file As Long
    file = popen(command, "r")

    If file = 0 Then
        Exit Function
    End If

    While feof(file) = 0
        Dim chunk As String
        Dim read As Long
        chunk = Space(50)
        read = fread(chunk, 1, Len(chunk) - 1, file)
        If read > 0 Then
            chunk = Left$(chunk, read)
            execShell = execShell & chunk
        End If
    Wend

    exitCode = pclose(file)
End Function

注意

  • マッチするものがなければ、何も返しません。
  • 深刻なエラーが起きた場合のみ、セルにはエラーが表示されます。
  • PHP の preg_match_all() を使用しています。正規表現のテストだけをしたい場合は、下記のようなサイトをご利用ください。

正規表現チェッカー PHP: preg_match() / JavaScript: match()

  • Big Sur でもし動かない場合は Microsoft が対応してくれるのを待つしかないかもしれません。

Missing librairies in /usr/lib on … | Apple Developer Forums

おわりに

  • PHP を使わずにできそうな気がするので、頑張ってみたい。
  • 頑張れば Mac / Windows 両対応のものも作れると思いますが、それは気が向いたら。

 頑張らなくてもそのうち Microsoft が標準で対応しような気がしますが...。

Excel や Numbers からコピーした表を Markdown としてペーストする【CotEditor】

Excel や Numbers、Google Spreadsheet からコピーした表を Markdown としてペーストする【CotEditor】

はじめに

 昨今の Markdown 界はどうなっているだろうかとネットサーフィンをしていると、最近はこのようなエディタが人気らしいではないですか。

 ふむふむ、これは良い WYSIWYG ですね。ていうか、ここまでする?

表のペースト機能がすごい

 「Excel の表をコピーしてペーストすると自動的に Markdown の表になる」という機能に驚かされました。

 これは良い。なんという逆転の発想。コピーするときに Markdown 文字列に変換するのではなく、ペーストする瞬間に処理を行う。これなら Excel だろうが Numbers だろうが Google Spreadsheet だろうが関係ありません。好きなスプレッドシートを使うことができます。もう、あっちは VBA、こっちは AppleScript で、向こうは JavaScript なんて考えなくて済みます。

何を驚いているんだい?そもそも初めから本来こうあるべきだろう?

 そんなスティーブ = ジョブズの名言が聞こえてくるようです。1

CotEditor スクリプトを作ってみる

 クリップボードから TSV 文字列を取り出すのは pbpaste コマンドがやってくれますから、それを加工する処理だけ、好きなスクリプト言語を使って作れます。

 ファイル名を「TSVを表としてペースト.@$v.php」なんてファイル名で CotEditor のスクリプトフォルダに入れれば、「⌘ + Shift + V」で実行できます。

使い方

 表計算ソフトからセル範囲を選択して「コピー」を行います。

 CotEditor に戻って「⌘ + Shift + V」します。以下のようになります。

Id    |Name            |Price       
------|----------------|------------
1     |Apple           |200         
2     |Banana          |150         
3     |Citrus          |300         
九九九|ドラゴンフルーツ|価格未設定。

スクリプトソースコード

#!/usr/bin/php -q
<?php
// %%%{CotEditorXInput=Selection}%%%
// %%%{CotEditorXOutput=ReplaceSelection}%%%

mb_detect_order("UTF-8,SJIS,sjis-win");

// 文字列の長さを、全角:2、半角:1 で数える。
function jp_str_len($str) {
    $len = 0;
    
    foreach (preg_split('//u', $str, null, PREG_SPLIT_NO_EMPTY) as $chr) {
        if (mb_ereg_match("[ -~]", $chr)) {
            $len += 1;
        } else {
            $len += 2;
        }
    }
    
    return $len;
}

// 文字列の長さを、全角:2、半角:1 として PAD する。
function jp_str_pad($input, $pad_length, $pad_string = " ", $pad_style=STR_PAD_RIGHT) {
    $input_len = jp_str_len($input);
    $pad_len = $pad_length - $input_len;
    $pad = str_repeat($pad_string, $pad_len);
    $ret = "";
    if ($pad_style === STR_PAD_RIGHT) {
        $ret = $input . $pad;
    } else {
        $ret = $pad . $input;
    }
    return $ret;
}

// ペーストボード取得
$pb = mb_convert_encoding(`pbpaste`, 'UTF-8', 'SJIS');
//$pb = `pbpaste`;

// 入力チェック
if (strpos($pb, "\n") === false && strpos($pb, "\t") === false) {
    $msg = "改行/タブのいずれも含まれていないようです。";
    $title = "クリップボードの内容は TSV ではありません";
    `osascript -e 'display notification "$msg" with title "$title"'`;
    exit(0);
}

// 行分割
$linesSrc = explode("\n", $pb);

// 列分割
$lines = [];
foreach ($linesSrc as $line) {
    $lines[] = explode("\t", $line);
}

// 列幅取得
$colWidth = [];
for ($i = 0; $i < count($lines[0]); $i++) {
    foreach ($lines as $line) {
        $c = $line[$i];
        $w = jp_str_len($c);
        $colWidth[$i] = max($w, $colWidth[$i]);
    }
}

// 配列全体 PAD 関数定義
$pad_string = ' ';
$array_pad_fnc = function ($text, $cidx) use ($colWidth, &$pad_string) {
    $pad = jp_str_pad($text, $colWidth[$cidx], $pad_string, STR_PAD_RIGHT);
    return $pad;
};

// 出力関数定義
function echo_markdown_line($ary) {
    echo implode('|', $ary) . "\n";
}

// 見出し出力
$titles = array_map($array_pad_fnc, $lines[0], range(0, count($lines[0])-1));
echo_markdown_line($titles);

// 水平線出力
$pad_string = '-';
$tmp = array_fill(0, count($colWidth), '-');
$hr = array_map($array_pad_fnc, $tmp, range(0, count($tmp)-1));
echo_markdown_line($hr);

// データ出力
$pad_string = ' ';
for ($i = 1; $i < count($lines); $i++) {
    $colTxt = $lines[$i];
    $cols = array_map($array_pad_fnc, $lines[$i], range(0, count($lines[$i])-1));
    echo_markdown_line($cols);
}

環境

 以下の環境で開発・動作確認しました。

ProductName:   Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H1323
PHP 7.3.11 (cli) (built: Jun  5 2020 23:50:40) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.11, Copyright (c) 1998-2018 Zend Technologies
CotEditor version: 2.7.4
Numbers.app version: 10.3.9

 また、Google Spreadsheet でも確認しました。

 CotEditor のスクリプトで動かした時のみ、pbpaste の結果を SJIS から UTF-8 に変換しないといけないのが、何となく納得できませんでした。Numbers.app 以外のアプリケーションから取得すると、また違うのかもしれません。


  1. Disk Burner(Power to Burn)の発表時だと思いますが、出典を見つけることはできませんでした。

最近ブラウザで開いた Web ページの Markdown リンクを CotEditor に挿入する【Safari/Chrome/CotEditor/Markdown/AppleScript/Swift】

はじめに

 サポンテは CotEditor を用いて Markdown ドキュメントを書いていますが、Markdown のリンクを作るのにブックマークレットを使っていました。

 ですが、いちいちアプリケーションを切り替えるのと、クリップボードを経由するのがなんとなく煩わしく感じてきました。

 AppleScript を用いれば、Safari で開いているページから Markdown リンクを作成して編集中の CotEditor 書類に挿入できるはずです。

 誰かが既に作っていそうですが「AppleScript Safari CotEditor Markdown link」などのキーワードで軽く検索した限りでは、CotEditor 用のスクリプトは見つかりませんでした。

 しかし「アプリケーションからリンクを取得する」という点に限って言えば AppleScript はたくさんありました。

 これなんかすごいですね。スゴすぎる。:Get Front Mac App Title and URL and make a Markdown Link to it

SafariGoogle Chrome だけでいいかな

 今のところ Safari だけで用は足りるのですけれども、そのうち Google Chrome も併用することになるかもしれないで、上記のスクリプトから切り出して作ってみますね。

 ただ CotEditor スクリプトで動かすということは frontmost application は CotEditor なので「二番目以降のアプリケーション」を「順番に」取得しないといけない(笑)。

完成したスクリプト

 意外と梃子摺った...。Swift も併用しました。ここまでやる必要があったでしょうか。

 下記 Swift プログラムを .appList.swift として、その次の AppleScript と一緒に CotEditor のスクリプトフォルダに入れてください。

Swift による補助スクリプト

 起動中のドキュメントウィンドウを開いているアプリケーション一覧を上から順に取得する Swift スクリプトです。

 AppleScript から呼び出すので、次のようにしてください。

  • AppleScript の本体スクリプトと同じフォルダに配置してください。
  • このスクリプト自体はメニュに表示したくないので、ファイル名は .appList.swift としてください。
  • ターミナルで、実行権を付与してください。chmod a+x .appList.swift
  • ターミナルで、./.appList.swift としてアプリケーションの一覧が表示されれば OK です。
#!/usr/bin/swift

// Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)
// Target: x86_64-apple-darwin19.6.0

import Foundation
import Cocoa

// 起動中のウィンドウのプロセス一覧を取得する
let options = CGWindowListOption(arrayLiteral: CGWindowListOption.excludeDesktopElements, CGWindowListOption.optionOnScreenOnly)
let windowList = CGWindowListCopyWindowInfo(options, CGWindowID(0))
let windows = windowList as NSArray? as! [[String: AnyObject]]

var ids = [pid_t]()

for window in windows {
    let pid = pid_t(window[kCGWindowOwnerPID as String]! as! Int)
    ids.append(pid)
}

// 重複行を出さないための配列を初期化する
var apps = [NSRunningApplication]()

// プロセス ID からアプリケーション名に変換して出力していく
for id in ids {
    // 変換
    let app = NSWorkspace.shared.runningApplications.filter { (app) -> Bool in
        app.activationPolicy == .regular && id == app.processIdentifier
    }.first
    
    // 変換結果の確認
    if app == nil {
        continue
    }
    
    // 既に出力済みかどうか確認する
    if apps.contains(app!) {
        continue
    }
    
    // アプリケーション名を出力する
    print(app?.bundleIdentifier ?? "")
    
    // 出力済みアプリケーションを記録しておく
    apps.append(app!)
}

AppleScript による CotEditor スクリプト

 本体スクリプトです。スクリプトエディタにコピペして、スクリプトとして CotEditor のスクリプトフォルダに保存してください。ファイル名はなんでも良いです。CotEditor のスクリプトメニューのメニュー名になります。

-- 開発環境:
-- AppleScript 2.7
-- CotEditor 4.0.2(457)
-- macOS Catalina 10.15.7

set theApplication to ""
set theText to ""
set theBody to ""

-- コマンドを実行して、ドキュメントウィンドウを開いているアプリケーションのリストを取得する
set current_path to ""
tell application "Finder"
    set current_path to (POSIX path of (container of (path to me) as alias))
end tell

log current_path

set r_path to current_path & ".appList.swift"
set appListSrc to do shell script "/usr/bin/swift " & "'" & r_path & "'"
set appList to paragraphs of appListSrc

log appList

-- 最近操作したブラウザウィンドウから、URL とタイトルを取得する
repeat with theApplication in appList
    log (theApplication as string = "com.google.Chrome")
    if theApplication as string = "com.google.Chrome" then
        tell application id "com.google.Chrome"
            using terms from application "Google Chrome"
                set theText to title of active tab of first window
                set theBody to get URL of active tab of first window
            end using terms from
        end tell
        exit repeat
    else if theApplication as string = "com.apple.Safari" then
        tell application id "com.apple.Safari"
            using terms from application "Safari"
                set theTab to front document
                set theText to name of theTab
                set theBody to URL of theTab
            end using terms from
        end tell
        exit repeat
    end if
end repeat

if theBody = "" then
    display notification "CotEditor の次に有効なアプリケーションが対応ブラウザではないようです。" with title "ブラウザ不明"
end if

-- CotEditor に Markdown を挿入する
tell application "CotEditor"
    if not (exists front document) then
        make new document
    end if
    
    set selectedText to contents of selection of front document
    if selectedText /= "" then
        set theText to selectedText
    end if
    
    tell front document
        set contents of selection to ("[" & theText & "](" & theBody & ")")
    end tell
end tell

使い方

 スクリプトを起動すると、開いているブラウザ画面でもっとも近いものからタイトルと URL を取得して CotEditor に挿入します。

 CotEditor で選択中の文字列があると、それをタイトルとして使用します。

参考

ワークシート上のセル選択範囲を画像ファイルとして保存【Excel/VBA/Windows】

はじめに

 先日の記事の姉妹版です。

 Excel のセルの選択範囲__セル範囲自体も、その上に描画されている図形なども__を画像ファイルとして出力します。Excel の「カメラ」機能で切り出した図をファイルにする感じですね。

 これもネットで調べると、同じような方法が見つかりますが、先日のマクロと同じく、A1 セルにパスを入力しておく形で実現します。

続きを読む

セル・セル範囲が選択されているか確認する【Excel/VBA】

はじめに

 VBA を書いていると、処理対象の Selection オブジェクトが、想定しているものであるかどうか調べたいことがあります。

 図形Shapeかどうかの確認については、以下のサイトがありました。

図形が選択されているか判定する-VarType関数・ShapeRangeプロパティ:エクセルマクロ・Excel VBAの使い方/DrawingObjectsオブジェクト

 セル範囲が選択状態にあるかどうかを判定するものは見つかりませんでした。「図形が選択されていなければ」という判定では不十分の可能性があります。グラフオブジェクトかもしれませんしね。

 ということで「セルまたはセル範囲が選択されている状態であるかどうか」を真偽値Booleanで返す関数を作ってみました。

続きを読む