2023年12月31日日曜日

Powershellで計算ゲーム


PowerShellの学習のためテンパズルっぽいものを解いてみる。



スペース区切りで数字を入れ、最後の数字に計算結果となる数字を入れると計算式がなりたつようなものを作ってみる。

例えば入力が 1 2 3 4 5 6 とすると

((1+2)*((3+4)-5))=6

((1+2)*(3+(4-5)))=6

のような表示をするようなものを作ってみる。


そのなかで、PowerShellネイティブ型とdotnetのクラスでの相互のやり取りがどのぐらいスムーズにいくのかを体感してみる。


ロジックでは、

数字の並びと演算子の並びを別々に考えたほうが時間短縮になるかとおもうので、あえて逆ポーランド記法は使わない。

ただ計算途中の記憶にスタック構造を使うし、二項演算子の2つをスタックに積むので考え方は同じ。


まずは、何も工夫もせずに作った例。


#計算ゲームver1

function 数列の作成{
    param(
    $数字候補,
    $組み立て済数列
    )
    process{

        if ($数字候補.Count -gt 0){
            for($i=0;$i -lt $数字候補.Count;$i++){
                $取り出し数字=$数字候補[$i]
                $取り出し済の数字候補=$数字候補.Clone()
                $取り出し済の数字候補.RemoveAt($i)
                数列の作成 $取り出し済の数字候補 ($組み立て済数列+$取り出し数字)
            }
        } else {
           #完成数字列の登録
           $完成数字列.Add($組み立て済数列) | Out-Null
        }
    }
}

function 演算子列の作成 {
    param(
      $演算子列,
      $保留を使用した数,
      $ストックされた数,
      $残り必要な演算子数
    )
    process{
        if ($残り必要な演算子数 -gt 0 ){
           foreach ($演算子 in ("T","H","K","W", "S")){
             #前2つの数字に対する演算方法の取り決め
             #Sは演算の保留
             if ($演算子 -eq "S"){
               #演算の保留ができない場合(後に数字がない場合)は保留禁止
               # 残りの数字の数は、問題の数字の数-保留を行った数
               if ($問題の数字の数 -le $保留を使用した数){ continue}
               演算子列の作成 ($演算子列 + "S" ) ($保留を使用した数 + 1) ($ストックされた数 + 1) $残り必要な演算子数
             } else {
               #演算ができない場合は、演算禁止
               #ストックされた数が2以上でないと演算できない
               if($ストックされた数 -lt 2 ) { continue}
                 演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
             }
          }
        }
        else {
          $完成版演算子列.Add($演算子列) | Out-Null
        }
    }
}

#使う数字と答えを入れてもらう

$inputT=Read-Host
$input=[int[]]($inputT -split " ")
$inputnum,$ans=[System.Collections.ArrayList]::new($input[0..($input.Length-2)]),$input[-1]


$完成版演算子列 = [System.Collections.ArrayList]::new()
$完成数字列 = [System.Collections.ArrayList]::new()
$問題の数字の数=$inputnum.Count

#演算子列と数列を作成する。
演算子列の作成 "" 0 0 ($inputnum.Count-1) #2項演算子のため2つめの数字に対する演算はスキップ"S"で、演算子数は数字の個数-1
数列の作成 $inputnum @()
"====="

#答え合わせ
foreach ($今回の数字の並び in $完成数字列){
  foreach ($今回の演算子の並び in $完成版演算子列){
    $tmp数字列=$今回の数字の並び #Clone()
    $numIndex=0
    $ngflg=$false
    $数式stack=[System.Collections.Stack]::new()
    $stack=[System.Collections.Stack]::new()
    for ($index = 0;$index -lt $今回の演算子の並び.Length;$index ++){
      switch($今回の演算子の並び[$index]){
      "S" { 
        $stack.Push($tmp数字列[$numIndex])
        $数式stack.Push($今回の数字の並び[$numIndex].ToString())
        $numIndex++
      }
      default {
        $op2=$stack.Pop() ; $数式op2=$数式stack.Pop()
        $op1=$stack.Pop() ; $数式op1=$数式stack.Pop()
        switch($_){
        #数式の計算はすべて()付きで行う。
        "T"{ $stack.Push($op1+$op2); $数式stack.Push("("+$数式op1+"+"+$数式op2+")")}
        "H"{ $stack.Push($op1-$op2); $数式stack.Push("("+$数式op1+"-"+$数式op2+")")}
        "K"{ $stack.Push($op1*$op2); $数式stack.Push("("+$数式op1+"*"+$数式op2+")")}
        "W"{ 
            #0で割る事態が発生したら誤答とするが、とりあえずダミーの計算を入れる。

          if($op2 -eq 0){
            $ngflg=$true
            $stack.Push($op1)
          } else{
            $stack.Push($op1/$op2)

          }
          $数式stack.Push("("+$数式op1+"/"+$数式op2+")")
        }
      }
      }
    }
  }
  $op=$stack.Pop()
  $数式op=$数式stack.Pop()
  #小数点の計算には誤差が発生するため小数点第4桁まで一致で妥協する。
  if(!$ngflg -and ([Math]::Round($op,4) -eq[Math]::Round($ans,4)) ){
    "$数式op=$ans :数列→$tmpans $($今回の数字の並び -join ",")  演算子列→$今回の演算子の並び"
    if ((Invoke-Expression $数式op) -ne  $ans){
    "審議!"
    } 
    #"-"
    #$今回の数字の並び
    #$今回の演算子の並び
    #"--"
  }
  }
}

次に、計算式に無駄な括弧が多いので、不要な括弧を除く対応をしてみる。
足し算
(a+b)+(c+d)=a+b+c+d
(a-b)+(c-d)=a-b+c-d
(a*b)+(c*d)=a*b+c*d
(a/b)+(c/d)=a/b+c/d
足し算は、プラスの左側右側ともに括弧を外せる。
引き算
(a+b)-(c+d)=a+b-(c+d)≠a+b-c+d
(a-b)-(c-d)=a-b-(c-d)≠a-b-c-d
(a*b)-(c*d)=a*b-c*d
(a/b)-(c/d)=a/b-c/d
引き算は、マイナスの左側は括弧が外せるが、右側はプラスとマイナスがある場合は括弧が必要。
掛け算
(a+b)*(c+d)=(a+b)*(c+d)≠...
(a-b)*(c-d)=(a-b)*(c-d)≠...
(a*b)*(c*d)=a*b*c*d
(a/b)*(c/d)=a/b*c/d
掛け算は、プラスとマイナスの場合、左右とも括弧が必要で、掛けると割るは括弧が外せる。
割り算
(a+b)/(c+d)=(a+b)/(c+d)≠...
(a-b)/(c-d)=(a-b)/(c-d)≠...
(a*b)/(c*d)=a*b/(c*d)≠...
(a/b)/(c/d)=a/b/(c/d)≠...
割り算は、プラスとマイナスの場合、左右とも括弧が必要で、掛けると割るの場合は、左側は括弧が不要で右側に括弧が必要。

この判定としては、演算子の左側と右側のともに括弧の部分を除いた部分の判定となる。
括弧の部分は判定不要なので、括弧を取り除いた部分での判定となり、正規表現を使った置換で最外殻の部分を判定する。今回は.netフレームワークの正規表現なので、pythonやrubyなどの再帰を使えない。.netフレームワークの正規表現ではその代わりにキャプチャする変数をスタックとして利用できるので、それを利用する。

また、不要な括弧を除くと、前出力した計算式と今回の計算式が同じとなることがあるので、結果計算式が同じとなった場合は2個目以降の表示を抑止することにする。

#計算ゲームver2
#ver1からの改良点:不要な括弧をのぞき、結果として同じ数式となった場合の重複排除


function 数列の作成{
    param(
    $数字候補,
    $組み立て済数列
    )
    process{

        if ($数字候補.Count -gt 0){
            for($i=0;$i -lt $数字候補.Count;$i++){
                $取り出し数字=$数字候補[$i]
                $取り出し済の数字候補=$数字候補.Clone()
                $取り出し済の数字候補.RemoveAt($i)
                数列の作成 $取り出し済の数字候補 ($組み立て済数列+$取り出し数字)
            }
        } else {
           #完成数字列の登録
           $完成数字列.Add($組み立て済数列) | Out-Null
        }
    }
}

function 演算子列の作成 {
    param(
      $演算子列,
      $保留を使用した数,
      $ストックされた数,
      $残り必要な演算子数
    )
    process{
        if ($残り必要な演算子数 -gt 0 ){
           foreach ($演算子 in ("T","H","K","W", "S")){
             #前2つの数字に対する演算方法の取り決め
             #Sは演算の保留
             if ($演算子 -eq "S"){
               #演算の保留ができない場合(後に数字がない場合)は保留禁止
               # 残りの数字の数は、問題の数字の数-保留を行った数
               if ($問題の数字の数 -le $保留を使用した数){ continue}
               演算子列の作成 ($演算子列 + "S" ) ($保留を使用した数 + 1) ($ストックされた数 + 1) $残り必要な演算子数
             } else {
               #演算ができない場合は、演算禁止
               #ストックされた数が2以上でないと演算できない
               if($ストックされた数 -lt 2 ) { continue}
                 演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
             }
          }
        }
        else {
          $完成版演算子列.Add($演算子列) | Out-Null
        }
    }
}

#使う数字と答えを入れてもらう

$inputT=Read-Host
$input=[int[]]($inputT -split " ")
$inputnum,$ans=[System.Collections.ArrayList]::new($input[0..($input.Length-2)]),$input[-1]


$完成版演算子列 = [System.Collections.ArrayList]::new()
$完成数字列 = [System.Collections.ArrayList]::new()
$検証済式の保存=[System.Collections.Generic.HashSet[string]]::new()
$問題の数字の数=$inputnum.Count

#演算子列と数列を作成する。
演算子列の作成 "" 0 0 ($inputnum.Count-1) #2項演算子のため2つめの数字に対する演算はスキップ"S"で、演算子数は数字の個数-1
数列の作成 $inputnum @()
"====="

#答え合わせ
foreach ($今回の数字の並び in $完成数字列){
  foreach ($今回の演算子の並び in $完成版演算子列){
    $tmp数字列=$今回の数字の並び #Clone()
    $numIndex=0
    $ngflg=$false
    $数式stack=[System.Collections.Stack]::new()
    $stack=[System.Collections.Stack]::new()
    for ($index = 0;$index -lt $今回の演算子の並び.Length;$index ++){
      switch($今回の演算子の並び[$index]){
      "S" { 
        $stack.Push($tmp数字列[$numIndex])
        $数式stack.Push($今回の数字の並び[$numIndex].ToString())
        $numIndex++
      }
      default {
        $op2=$stack.Pop() ; $数式op2=$数式stack.Pop()
        $op1=$stack.Pop() ; $数式op1=$数式stack.Pop()
        switch($_){
        "T"{ $stack.Push($op1+$op2); $数式stack.Push($数式op1+"+"+$数式op2)}
        "H"{ $stack.Push($op1-$op2);
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"-"+$数式op2)
           }
        "K"{ $stack.Push($op1*$op2); 
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op1="("+$数式op1+")"
             }
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"*"+$数式op2)
           }

        "W"{ 
             #0で割る事態が発生したら誤答とするが、とりあえずダミーの計算を入れる。
             if($op2 -eq 0){
               $ngflg=$true
               $stack.Push($op1)
             } else{
               $stack.Push($op1/$op2)
             }
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op1="("+$数式op1+")"
             }
             # op2のカッコの外側で+もしくは-もしくは/が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-|/|\*"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"/"+$数式op2)
           }
        }
      }
      }
    }

    $op=$stack.Pop()
    $数式op=$数式stack.Pop()
    #小数点の計算には誤差が発生するため小数点第4桁まで一致で妥協する。
    if(!$ngflg -and ([Math]::Round($op,4) -eq[Math]::Round($ans,4)) -and !($検証済式の保存.Contains($数式op))){
      $検証済式の保存.Add($数式op) | Out-Null
      "$数式op=$ans :数列→$tmpans $($今回の数字の並び -join ",")  演算子列→$今回の演算子の並び"
      if ((Invoke-Expression $数式op) -ne  $ans){
      "審議!"
      } 
    }  
  }
}


最後に割り算の計算誤差をごまかしていた部分の対応。
結局分数の形で割り算を扱えば、分母分子がそれぞれ整数でおさまるので、これで誤差の処理が不要となる。

#計算ゲームver3
#ver2からの改良点:割り算を分数として表現し、誤差補正処理を排除する
#分数表現は2項目の配列とし1個目を分子。2個目を分母とする。
#足し算引き算では通分演算する。約分はしない(lcd,gcdを使わない)

function 数列の作成{
    param(
    $数字候補,
    $組み立て済数列
    )
    process{

        if ($数字候補.Count -gt 0){
            for($i=0;$i -lt $数字候補.Count;$i++){
                $取り出し数字=$数字候補[$i]
                $取り出し済の数字候補=$数字候補.Clone()
                $取り出し済の数字候補.RemoveAt($i)
                数列の作成 $取り出し済の数字候補 ($組み立て済数列+$取り出し数字)
            }
        } else {
           #完成数字列の登録
           $完成数字列.Add($組み立て済数列) | Out-Null
        }
    }
}

function 演算子列の作成 {
    param(
      $演算子列,
      $保留を使用した数,
      $ストックされた数,
      $残り必要な演算子数
    )
    process{
        if ($残り必要な演算子数 -gt 0 ){
           foreach ($演算子 in ("T","H","K","W", "S")){
             #前2つの数字に対する演算方法の取り決め
             #Sは演算の保留
             if ($演算子 -eq "S"){
               #演算の保留ができない場合(後に数字がない場合)は保留禁止
               # 残りの数字の数は、問題の数字の数-保留を行った数
               if ($問題の数字の数 -le $保留を使用した数){ continue}
               演算子列の作成 ($演算子列 + "S" ) ($保留を使用した数 + 1) ($ストックされた数 + 1) $残り必要な演算子数
             } else {
               #演算ができない場合は、演算禁止
               #ストックされた数が2以上でないと演算できない
               if($ストックされた数 -lt 2 ) { continue}
                 演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
             }
          }
        }
        else {
          $完成版演算子列.Add($演算子列) | Out-Null
        }
    }
}

function 足す {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        ($被数分子*$原数分母+$原数分子*$被数分母) , ($被数分母*$原数分母) 
    }
}

function 引く {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        ($被数分子*$原数分母-$原数分子*$被数分母) , ($被数分母*$原数分母) 
    }
}
function 掛ける {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        ($被数分子*$原数分子) , ($被数分母*$原数分母) 
    }
}

function 割る {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        ($被数分子*$原数分母) , ($被数分母*$原数分子) 
    }
}


#使う数字と答えを入れてもらう

$inputT=Read-Host
$input=[int[]]($inputT -split " ")
$inputnum,$ans=[System.Collections.ArrayList]::new($input[0..($input.Length-2)]),$input[-1]


$完成版演算子列 = [System.Collections.ArrayList]::new()
$完成数字列 = [System.Collections.ArrayList]::new()
$検証済式の保存=[System.Collections.Generic.HashSet[string]]::new()
$問題の数字の数=$inputnum.Count

#演算子列と数列を作成する。
演算子列の作成 "" 0 0 ($inputnum.Count-1) #2項演算子のため2つめの数字に対する演算はスキップ"S"で、演算子数は数字の個数-1
数列の作成 $inputnum @()
"====="

#答え合わせ
foreach ($今回の数字の並び in $完成数字列){
  foreach ($今回の演算子の並び in $完成版演算子列){
    $tmp数字列=$今回の数字の並び #Clone()
    $numIndex=0
    $ngflg=$false
    $数式stack=[System.Collections.Stack]::new()
    $stack=[System.Collections.Stack]::new()
    for ($index = 0;$index -lt $今回の演算子の並び.Length;$index ++){
      switch($今回の演算子の並び[$index]){
      "S" { 
        $stack.Push(@($tmp数字列[$numIndex],1))
        $数式stack.Push($今回の数字の並び[$numIndex].ToString())
        $numIndex++
      }
      default {
        $op2=$stack.Pop() ; $数式op2=$数式stack.Pop()
        $op1=$stack.Pop() ; $数式op1=$数式stack.Pop()
        switch($_){
        "T"{ $stack.Push((足す $op1 $op2)); $数式stack.Push($数式op1+"+"+$数式op2)}
        "H"{ $stack.Push((引く $op1 $op2))
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"-"+$数式op2)
           }
        "K"{ $stack.Push((掛ける $op1 $op2)) 
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op1="("+$数式op1+")"
             }
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"*"+$数式op2)
           }

        "W"{ 
             #0で割る事態が発生したら誤答とするが、とりあえずダミーの計算を入れる。
             if($op2[0] -eq 0){
               $ngflg=$true
               $stack.Push($op1)
             } else{
               $stack.Push((割る $op1 $op2))
             }
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-"){
               $数式op1="("+$数式op1+")"
             }
             # op2のカッコの外側で+もしくは-もしくは/が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'-open'\)))*(?(open)(?!))\)" ) -match "\+|-|/|\*"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push($数式op1+"/"+$数式op2)
           }
        }
      }
      }
    }

    $op=$stack.Pop()
    $数式op=$数式stack.Pop()
    if(!$ngflg -and $op[0] -eq ($ans * $op[1]) -and !($検証済式の保存.Contains($数式op))){
      $検証済式の保存.Add($数式op) | Out-Null
      "$数式op=$ans :数列→$tmpans $($今回の数字の並び -join ",")  演算子列→$今回の演算子の並び"
      if ((Invoke-Expression $数式op) -ne  $ans){
      "審議!"
      "$ans,op=$op, op0=$($op[0]),op1=$($op[1])"
      } 
    }  
  }
}



ま、ここまでかなぁ。
最近これを見て作ってみたけど
2 3 6 6 を使って19を作るものって本当に1パターンしかないみたい。
(順番は変えて4つは出ているけど、すべて同じような計算でした。)
2 3 6 6 19
=====
(2/6+6)*3=19 :数列→ 2,6,6,3  演算子列→SSWSTSK
3*(2/6+6)=19 :数列→ 3,2,6,6  演算子列→SSSWSTK
3*(6+2/6)=19 :数列→ 3,6,2,6  演算子列→SSSSWTK
(6+2/6)*3=19 :数列→ 6,2,6,3  演算子列→SSSWTSK

-replace の後に検索文字列しか書いておらず置換文字列を書き忘れてた。検索文字列を空文字で置き換える(検索文字列を削除する)ことをしたかったのだけど、わかりずらかったかも。
もう1つ補足
演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
の部分の($ストックされた数 - 1 )というのは、2つの数字をスタックから取り出して演算した後、演算結果をスタックに戻すので、-2+1で-1です。


上でやめようと思ったけど、、
-replaceをちゃんと書いたものとやっぱり既約分数化しておいたものを書いておきます。
ただ、作成しているプログラムがどんどんだらしなくなってきているのでこれで最後かな?

数字列をパイプラインに流して、数字列が出来上がった直後からチェックが走るようにしようとしてみたけど、書き出しも速くなっていない。数字列を作る処理よりも(最初でまとめて行う)演算子の組み合わせを作る処理のほうがはるかに時間がかかるからといえばそれまでなんだけど、演算子と数字のならべかたを別々にしちゃったからなぁ。逆ポーランド記法のように数字と演算子の並びを一緒にやったら計算開始が早くなったのかもしれません。
#計算ゲームver4
#ver2からの改良点:割り算を分数として表現し、誤差補正処理を排除する
#分数表現は2項目の配列とし1個目を分子。2個目を分母とする。
#足し算引き算では通分演算する。計算結果を既約分数にする
#数字列をパイプラインでつなげて後続処理に流す。

function 数列の作成{
    param(
    $数字候補,
    $組み立て済数列
    )
    process{

        if ($数字候補.Count -gt 0){
            for($i=0;$i -lt $数字候補.Count;$i++){
                $取り出し数字=$数字候補[$i]
                $取り出し済の数字候補=$数字候補.Clone()
                $取り出し済の数字候補.RemoveAt($i)
                数列の作成 $取り出し済の数字候補 ($組み立て済数列+$取り出し数字)
            }
        } else {
           #完成数字列の出力,配列にしてしまうと各要素ごとにパイプライン処理が走りそうなので配列をオブジェクトでくるむ
           @{完成数字列=$組み立て済数列}
        }
    }
}

function 演算子列の作成 {
    param(
      $演算子列,
      $保留を使用した数,
      $ストックされた数,
      $残り必要な演算子数
    )
    process{
        if ($残り必要な演算子数 -gt 0 ){
           foreach ($演算子 in ("T","H","K","W", "S")){
             #前2つの数字に対する演算方法の取り決め
             #Sは演算の保留
             if ($演算子 -eq "S"){
               #演算の保留ができない場合(後に数字がない場合)は保留禁止
               # 残りの数字の数は、問題の数字の数-保留を行った数
               if ($問題の数字の数 -le $保留を使用した数){ continue}
               演算子列の作成 ($演算子列 + "S" ) ($保留を使用した数 + 1) ($ストックされた数 + 1) $残り必要な演算子数
             } else {
               #演算ができない場合は、演算禁止
               #ストックされた数が2以上でないと演算できない
               if($ストックされた数 -lt 2 ) { continue}
                 演算子列の作成 ($演算子列 + $演算子) ($保留を使用した数) ($ストックされた数 - 1) ($残り必要な演算子数 -1)
             }
          }
        }
        else {
          $完成版演算子列.Add($演算子列) | Out-Null
        }
    }
}
function 最大公約数{
  $a,$b=[Math]::abs($args[0][0]),[Math]::abs($args[0][1])
  $a,$b = if ($a -gt $b ){$a,$b} else{$b,$a}
  while ($b -ne 0){
    $a,$b=$b,($a % $b)
  }
  $a
}
function 既約等{
  $a,$b=($args[0][0]),($args[0][1])
  if ($b -lt 0) { $a,$b=(-1*$a),(-1*$b)}
  $r=最大公約数 $a,$b
  ($a / $r ),($b / $r)
}

function 足す {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        既約等 ($被数分子*$原数分母+$原数分子*$被数分母) , ($被数分母*$原数分母) 
    }
}

function 引く {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        既約等 ($被数分子*$原数分母-$原数分子*$被数分母) , ($被数分母*$原数分母) 
    }
}
function 掛ける {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        既約等 ($被数分子*$原数分子) , ($被数分母*$原数分母) 
    }
}

function 割る {
    param(
      $被数,
      $原数
    )
    process{
        $被数分子,$被数分母=$被数
        $原数分子,$原数分母=$原数
        既約等 ($被数分子*$原数分母) , ($被数分母*$原数分子)
    }
}


#使う数字と答えを入れてもらう

$inputT=Read-Host
$input=[int[]]($inputT -split " ")
$inputnum,$ans=[System.Collections.ArrayList]::new($input[0..($input.Length-2)]),$input[-1]


$完成版演算子列 = [System.Collections.ArrayList]::new()
$検証済式の保存=[System.Collections.Generic.HashSet[string]]::new()
$問題の数字の数=$inputnum.Count

#演算子列と数列を作成する。
演算子列の作成 "" 0 0 ($inputnum.Count-1) #2項演算子のため2つめの数字に対する演算はスキップ"S"で、演算子数は数字の個数-1
数列の作成 $inputnum @()|%{
#答え合わせ
   $今回の数字の並び=$_.完成数字列
  foreach ($今回の演算子の並び in $完成版演算子列){
    $tmp数字列=$今回の数字の並び #Clone()
    $numIndex=0
    $ngflg=$false
    $数式stack=[System.Collections.Stack]::new()
    $stack=[System.Collections.Stack]::new()
    for ($index = 0;$index -lt $今回の演算子の並び.Length;$index ++){
      switch($今回の演算子の並び[$index]){
      "S" { 
        $stack.Push(@($tmp数字列[$numIndex],1))
        $数式stack.Push($今回の数字の並び[$numIndex].ToString())
        $numIndex++
      }
      default {
        $op2=$stack.Pop() ; $数式op2=$数式stack.Pop()
        $op1=$stack.Pop() ; $数式op1=$数式stack.Pop()
        switch($_){
        #数式の計算はすべて()付きで行う。
        "T"{ $stack.Push((足す $op1 $op2)); $数式stack.Push("$数式op1+$数式op2")}
        "H"{ $stack.Push((引く $op1 $op2))
             # op2のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)","" ) -match "\+|-"){
               $数式op2="($数式op2)"
             }
             $数式stack.Push("$数式op1-$数式op2")
           }
        "K"{ $stack.Push(@(掛ける $op1 $op2)) 
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)","" ) -match "\+|-"){
               $数式op1="($数式op1)"
             }
             # op2のカッコの外側で+もしくは-が使われていた場合、をかっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)","" ) -match "\+|-"){
               $数式op2="("+$数式op2+")"
             }
             $数式stack.Push("$数式op1*$数式op2")
           }

        "W"{ 
             #0で割る事態が発生したら誤答とするが、とりあえずダミーの計算を入れる。
             if($op2[0] -eq 0){
               $ngflg=$true
               $stack.Push($op1)
             } else{
               $stack.Push(@(割る $op1 $op2))
             }
             # op1のカッコの外側で+もしくは-が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op1 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)","" ) -match "\+|-"){
               $数式op1="($数式op1)"
             }
             # op2のカッコの外側で+もしくは-もしくは/が使われていた場合、かっこでくくる。そうではない場合かっこでくくらない
             if (($数式op2 -replace "\(([^()]|(?'open'\()|(?'open'\)))*(?(open)(?!))\)" ) -match "\+|-|/|\*"){
               $数式op2="($数式op2)"
             }
             $数式stack.Push("$数式op1/$数式op2")
           }
        }
      }
      }
    }

    $op=$stack.Pop()
    $数式op=$数式stack.Pop()
    if(!$ngflg -and $op[0] -eq ($ans * $op[1]) -and !($検証済式の保存.Contains($数式op))){
      $検証済式の保存.Add($数式op) | Out-Null
      "$数式op=$ans :数列→$($今回の数字の並び -join ",")  演算子列→$今回の演算子の並び"
      if ((Invoke-Expression $数式op) -ne  $ans){
      "審議!"
      "$ans,op=$op, op0=$($op[0]),op1=$($op[1])"
      } 
    }  
  }
}
今後続けるとしたら逆ポーランド記法に書き換えるとパイプライン処理の出力開始が早くなるか、と同じ数字が複数回使われた時の数字列の組み合わせをちゃんと絞ることをするのかなぁ

2023年3月25日土曜日

Power BIのPower Queryで環境変数を取得する方法

Power Queryで、読み込みファイルに(相対パスではなく)絶対パスを要求する場面があるが、Power Query M言語では環境変数にアクセスできないように見える。
なので、Pythonを利用して環境変数をとってみる。
まず、何とかして、以下を実行できるようにしてみる。
https://learn.microsoft.com/ja-jp/power-bi/connect-data/desktop-python-in-query-editor

私の環境では、pip install pandasだけでは動かなくて、pip install matplotlibもしてみた。


そのあと、PowerQueryで適当な名前のクエリを作り以下の内容にしてみた。


let
    ソース =#table({"a"},{{0}}),
    PyExe = Python.Execute("dataset[""name""]=os.environ['USERPROFILE']",[dataset=ソース]),
    ExpandTable = Table.ExpandTableColumn(PyExe, "Value", {"name"}, {"name.1"}),
    ExpandValue =ExpandTable{0}[name.1]
in
    ExpandValue


解説
Python.Executeでは、テーブルの入力が必要らしいので、ソースに、ダミーのテーブル(列名がaで、中身が0の1行レコードのテーブル)を入れる。
ダミーのテーブルに対して、name列を追加して、その内容を環境変数USERPROFILEの展開後のものとした。
PythonのテーブルがValueに入るため、Valueのテーブル内にアクセスできるように展開した。
展開後のテーブルの1行目、name.1列の内容をExpandValueとし、出力結果をExpandValueとした。


洗練されてはおらずいろいろとゴミがあるが、
これで、このクエリの結果がUSERPROFILEの展開後の結果となった。

2020年10月3日土曜日

PowerShellでの配列の中のオブジェクトアクセス

以下試してみた結果、PowerShellの配列を正しく指定しなかった場合でもPowerShellが裏で仕事をして、いい感じにデータを採取してくれるみたい。 ただ更新の場合は、オブジェクト位置を正しく示している場合のみ、更新できるみたい。 正しく位置指定をしなかった場合、配列の添え字が入れられていないものは配列全体を走査して、見つかったものを返す。複数みつかれば配列として返してくれるみたい。だけど、当然というか、PowerShellが裏で仕事して引っ張ってきたデータについては、更新がエラーになったり無効となったりしている。 データ構造の理解があいまいだと、値書き換え時に大変なことになりそうな予感がする。 まあ、データ構造を完全把握していれば、データベース的に検索ができそうなきがするが。。。

PS > $a=((1,2,@{a=10;b=11}),(4,5,@{a=20}),(7,8,@{a=30;b=12}))
PS > $a.a
10
20
30
PS > $a.b
11
12
PS > $a.a.length
3
PS > $a.b.length
2
PS > $a.a[0]
10
PS > $a.a[0]=100
PS > $a.a[0]
10
PS > $a[0].a=100
このオブジェクトにプロパティ 'a' が見つかりません。プロパティが存在し、設定可能であることを確認してください。
発生場所 行:1 文字:1
+ $a[0].a=100
+ ~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) []、RuntimeException
    + FullyQualifiedErrorId : PropertyAssignmentException

PS > $a.a
10
20
30
PS > $a[0][2].a
10
PS > $a[0][2].a=100
PS > $a.a
100
20
30
PS > $a.b[1]
12
PS > $a[1].b
PS > $a[0].b
11
PS > $a[2].b
12
PS > $a[2].b=120
このオブジェクトにプロパティ 'b' が見つかりません。プロパティが存在し、設定可能であることを確認してください。
発生場所 行:1 文字:1
+ $a[2].b=120
+ ~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) []、RuntimeException
    + FullyQualifiedErrorId : PropertyAssignmentException

PS > $a.b[1]
12
PS > $a[1].b
PS > $a.b[1]
12
PS > $a.b[1]=120
PS > $a.b
11
12
PS > $a[2][2].b
12
PS > $a[2][2].b=120
PS > $a.b
11
120
PS >

このほか、forなどループ文にたいしても=で結果を変数に代入できるけれど、これはパイプと同じくループ文の塊の中で出力した結果が変数に入るみたい。 使いどころはあるのだけれど、powershellは癖が強いなぁ。 そういえば、メイン?で使っていたブログがサービス終了となっていましたので、つぶやきはこちらだけになりそうです。

2012年8月19日日曜日

DBの1対多の検索(アプリ側からの指定)

1対多の関係の属性値について、アプリの検索画面で、andとかorとか括弧とか使った検索をさせる場合にどういうようなテーブル設計にして、どのように検索させれば良いかということについて、とっさに何も思い浮かばなかったので、ちょっと考えてみた。

id1の値が2~10,id2がid1の倍数で最大12まであるゼロ埋め2桁のもの。
検索条件で、+をor,*をand、括弧が使えるもの。
で、id2の検索条件からid1を検索する場合、
条件が"04+06"で、結果が"2,3,4,6",
条件が"04*06"で、検索結果が、"2",
条件が"(06+08)*10"で、結果が、"2",
条件が"06+08*10"で、結果が、"2,3,5,6"
条件が"04*08*12"で、結果が、"2,4"
条件が、12*(05+06+(04*08))で、結果が、"2,3,4,6",
条件が、"05+06*(02+03)"で、id1の検索結果が、"2,3,5"

案1
テーブル構造:tbl(id1,id2)下線は主キー
データ:(2,'02_04_06_08_10_12'),(3,'03_06_09_12'),(4,'04_08_12'),(5,'05_10'),(6,'06_12'),(7,'07'),(8,'08'),(9,'09'),(10,'10')
sql文:
先頭部分は、固定で"select id1 from tbl where "
末尾に動的に追加、
条件が、値の場合、"id2 like '%値%'" を追加し、次の条件を読み取る。
条件が、"+"の場合、" or "を追加し、次の条件を読み取る。
条件が、"*"の場合、" and "を追加し、次の条件を読み取る。
条件が、"-"の場合、次の値まで読んで、id not like '%値%'"を追加し、次の条件を読み取る。
条件が、括弧の場合は、そのまま追加し、次の条件を読み取る。

案2
テーブル構造:tbl(id1,id2) 下線は主キー,インデックスの列の指定順はid2,id1の順
データ:(2,'02'),(2,'04'),(2,'06'),(2,'08'),(2,'10'),(2,'12'),(3,'03'),(3,'06'),(3,'09'),(3,'12'),(4,'04'),(4,'08'),(4,'12'),(5,'05'),(5,'10'),(6,'06'),(6,'12'),(7,'07'),(8,'08'),(9,'09'),(10,'10')
sql文:
固定先頭部分はなしで、
sql文を動的に追加、
条件が、値の場合、"select id1 from tbl where id2='値' "を追加し、次の条件を読み取る。
条件が、"+"の場合、"union "を追加し、次の条件を読み取る。
条件が、"*"の場合、"intersect "を追加し、次の条件を読み取る。
条件が、"-"の場合、"except "を追加し、次の条件を読み取る。
条件が、括弧の場合は、そのまま追加し、次の条件を読み取る。

で、いいのかな?

2012年5月12日土曜日

プロコン2012

今年のプロコン、androidアプリが題材のがある。 android端末が2つ持ってるので参加してみたいんだけど、今年は期限内に完成まで漕ぎ着ける見込みが立たないので今回も不参加。 ま、勉強がてら期限は設定せずにプロコンと同じ題材で取り組んでみようかな?

2010年9月12日日曜日

プロコン2010-1その8

ユーザの削除画面。
ユーザを削除すると、そのユーザに紐づく書籍を他の人に移管するかシステムから削除するようにするため、ちょっと考慮が必要でした。


で、DACは、前回書いたUserInfo.csなので今回は省略して、
UI(~/Admin/DeleteUser.aspx)から

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="DeleteUser.aspx.cs" Inherits="Library.Admin.DeleteUser"
MasterPageFile="~/Admins.Master" StylesheetTheme="Standard" %>

<asp:Content ID="Content1" runat="server" ContentPlaceHolderID="ContentPlaceHolder1">
<asp:Label ID="Label1" runat="server" Text=""></asp:Label>
<br />
削除対象のユーザを選択してください。<asp:RadioButtonList ID="RadioButtonList1" runat="server" DataSourceID="ObjectDataSource1"
DataTextField="UserName" DataValueField="UserName">
</asp:RadioButtonList>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="GetAllData"
TypeName="Library.DAC.UserInfo"></asp:ObjectDataSource>
<br />
書籍の引継ぎユーザを指定してください。<asp:DropDownList ID="DropDownList1" runat="server" DataSourceID="ObjectDataSource1"
DataTextField="UserName" DataValueField="UserName" AppendDataBoundItems="True">
<asp:ListItem Value="">引き継ぎ無し</asp:ListItem>
</asp:DropDownList>
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="Button1_Click" />
</asp:Content>



次にコードビハインド

using System;
using System.Web.Security;
using System.Web.UI;
using Library.DAC;

namespace Library.Admin
{
public partial class DeleteUser : LibPage
{
protected void Page_Load(object sender, EventArgs e)
{
Label1.Text = "";
}

protected void Button1_Click(object sender, EventArgs e)
{
Membership.DeleteUser(RadioButtonList1.SelectedValue);
UserInfo.InheritBookOwner(RadioButtonList1.SelectedValue, DropDownList1.SelectedValue);
UserInfo.DeleteUser(RadioButtonList1.SelectedValue);
Page.DataBind();
Label1.Text = "削除が完了しました";
}
}
}



本当は、ACIDの一貫性を保つために、UserInfo.InheritBookOwner()と、UserInfo.DeleteUser()を連続したよびだしし、トランザクションスコープをくくったUserInfoのメソッドで用意するべきだったけど、まいいかぁ。

プロコン2010-1その7

今回は、ユーザの変更の画面。
DataSetのコードは省略し、DACと、UIのコードを。
まずは、DAC(~/DAC/UserInfo.cs)から。

using System.Collections.Generic;
using System.Web.Security;
using Library.DataSet;
using Library.DataSet.LibraryDataSetTableAdapters;
namespace Library.DAC
{
public static class UserInfo
{
public static LibraryDataSet.UsersDataTable GetAllData()
{
LibraryDataSet.UsersDataTable dt = new LibraryDataSet.UsersDataTable();
using (UsersTableAdapter ta = new UsersTableAdapter())
{
dt = ta.GetData();
foreach (MembershipUser msu in Membership.GetAllUsers())
{
if ((dt.Select("UserName='" + msu.UserName + "'")).Length == 0)
{
dt.AddUsersRow(msu.UserName, string.Empty, false, false);
ta.Update(dt);
}
}
dt = ta.GetData();
foreach (LibraryDataSet.UsersRow ur in dt)
{
ur.AdminRoleFlag = Roles.IsUserInRole(ur.UserName, "Admins");
ur.UserRoleFlag = Roles.IsUserInRole(ur.UserName, "Users");
}
return dt;
}
}
public static void Update(string userName, string information, bool adminRoleFlag, bool userRoleFlag)
{
using (UsersTableAdapter ta = new UsersTableAdapter())
{
ta.Update(information, userName);
}
if (adminRoleFlag && !Roles.IsUserInRole(userName, "Admins"))
{
Roles.AddUserToRole(userName, "Admins");
}
else if (!adminRoleFlag && Roles.IsUserInRole(userName, "Admins"))
{
Roles.RemoveUserFromRole(userName, "Admins");
}
if (userRoleFlag && !Roles.IsUserInRole(userName, "Users"))
{
Roles.AddUserToRole(userName, "Users");
}
else if (!userRoleFlag && Roles.IsUserInRole(userName, "Users"))
{
Roles.RemoveUserFromRole(userName, "Users");
}
}
public static void InheritBookOwner(string oldOwner, string newOwner)
{
using (BookOwnerTableAdapter ta = new BookOwnerTableAdapter())
{
if (string.IsNullOrEmpty(newOwner))
{
ta.DeleteByUserName(oldOwner);
}
else
{
ta.InheritBookOwner(newOwner, oldOwner);
}
}
}
public static void DeleteUser(string userName)
{
using (UsersTableAdapter ta = new UsersTableAdapter())
{
ta.Delete(userName);
}
}
public static LibraryDataSet.UsersDataTable GetDataFirstByGivenUser(string GivenUserName)
{
using(UsersTableAdapter ta=new UsersTableAdapter())
{
return ta.GetDataFirstByGivenUserName(GivenUserName);
}
}
}
}



ユーザ変更のコード部分(~/Admin/ModifyUser.aspx.cs)


<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ModifyUser.aspx.cs" Inherits="Library.Admin.ModifyUser"
MasterPageFile="~/Admins.Master" StylesheetTheme="Standard" %>

<asp:Content ID="Content1" runat="server" ContentPlaceHolderID="ContentPlaceHolder1">
<asp:Label ID="Label1" runat="server" Text=""></asp:Label>
<asp:GridView ID="GridView1" runat="server" DataKeyNames="UserName" AutoGenerateColumns="False"
DataSourceID="ObjectDataSource1" OnRowUpdated="GridView1_RowUpdated" AllowPaging="True"
AllowSorting="True" CellPadding="4" ForeColor="#333333" GridLines="None">
<RowStyle BackColor="#FFFBD6" ForeColor="#333333" />
<Columns>
<asp:CommandField ShowEditButton="True" />
<asp:BoundField DataField="UserName" HeaderText="ユーザ名" ReadOnly="True" SortExpression="UserName" />
<asp:BoundField DataField="Information" HeaderText="ユーザ情報" SortExpression="Information" />
<asp:CheckBoxField DataField="AdminRoleFlag" HeaderText="管理者権限"
SortExpression="AdminRoleFlag" />
<asp:CheckBoxField DataField="UserRoleFlag" HeaderText="ユーザ権限"
SortExpression="UserRoleFlag" />
</Columns>
<FooterStyle BackColor="#990000" Font-Bold="True" ForeColor="White" />
<PagerStyle BackColor="#FFCC66" ForeColor="#333333" HorizontalAlign="Center" />
<SelectedRowStyle BackColor="#FFCC66" Font-Bold="True" ForeColor="Navy" />
<HeaderStyle BackColor="#990000" Font-Bold="True" ForeColor="White" />
<AlternatingRowStyle BackColor="White" />
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server" SelectMethod="GetAllData"
TypeName="Library.DAC.UserInfo" UpdateMethod="Update">
<UpdateParameters>
<asp:Parameter Name="userName" Type="String" />
<asp:Parameter Name="information" Type="String" />
<asp:Parameter Name="adminRoleFlag" Type="Boolean" />
<asp:Parameter Name="userRoleFlag" Type="Boolean" />
</UpdateParameters>
</asp:ObjectDataSource>
</asp:Content>



最後に、コードビハインド

using System;
using System.Web.Security;

namespace Library.Admin
{
public partial class ModifyUser : LibPage
{
protected void Page_Load(object sender, EventArgs e)
{
Label1.Text = "";
}

protected void GridView1_RowUpdated(object sender, System.Web.UI.WebControls.GridViewUpdatedEventArgs e)
{
Label1.Text = "更新しました";
}
}
}