ちょっとしたツールはGitHub Copilotに作らせよう(月10ドルの元を取ろう)

# きっかけ

+ PAD(PowerAutomate Desktop)で大量に撮ったスクショを処理したい
+ GitHub Copilotに月10ドル払っているのにほぼ補完機能しか使っていなかった(月によってはほぼ使ってないときもある)

## 重複する画像を削除

### プロンプト
>次のプログラムを作成
入力：ディレクトリパス。期待する結果全く同じ画像の削除。プログラムはGo言語で作成すること。

### 出力されたプログラム
```go
package main

import (
    "crypto/md5"
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
)

func main() {
    dir := "./path/to/your/directory" // ディレクトリパスを指定
    files, err := ioutil.ReadDir(dir)
    if err != nil {
        fmt.Println("Error reading directory:", err)
        return
    }

    hashes := make(map[[md5.Size]byte]string)
    for _, file := range files {
        if file.IsDir() {
            continue
        }

        filePath := filepath.Join(dir, file.Name())
        data, err := ioutil.ReadFile(filePath)
        if err != nil {
            fmt.Println("Error reading file:", err)
            continue
        }

        hash := md5.Sum(data)
        if existingFile, found := hashes[hash]; found {
            err := os.Remove(filePath)
            if err != nil {
                fmt.Println("Error deleting file:", err)
            } else {
                fmt.Printf("Deleted duplicate file: %s (same as %s)\n", filePath, existingFile)
            }
        } else {
            hashes[hash] = filePath
        }
    }
}
```

### 修正プロンプト
>コマンドライン引数でパスを指定できるように仕様を変更してください

>ioutilパッケージは非推奨です。修正してください

>使用するハッシュ関数を第一引数してできるようにしてください

>75行目のfmt.Printfをハッシュ値 (関数名+値)を出力するように変更。ただし75行目だけ出力

### 修正されたプログラム
```go
package main

import (
    "crypto/md5"
    "crypto/sha1"
    "crypto/sha256"
    "fmt"
    "hash"
    "io"
    "os"
    "path/filepath"
)

func getHashFunction(name string) (func() hash.Hash, string, error) {
    switch name {
    case "md5":
        return md5.New, "md5", nil
    case "sha1":
        return sha1.New, "sha1", nil
    case "sha256":
        return sha256.New, "sha256", nil
    default:
        return nil, "", fmt.Errorf("unsupported hash function: %s", name)
    }
}

func main() {
    if len(os.Args) != 3 {
        fmt.Println("Usage: go run main.go <hash_function> <directory_path>")
        return
    }

    hashFuncName := os.Args[1]
    dir := os.Args[2]

    hashFunc, hashFuncName, err := getHashFunction(hashFuncName)
    if err != nil {
        fmt.Println(err)
        return
    }

    files, err := os.ReadDir(dir)
    if err != nil {
        fmt.Println("Error reading directory:", err)
        return
    }

    hashes := make(map[string]string)
    for _, file := range files {
        if file.IsDir() {
            continue
        }

        filePath := filepath.Join(dir, file.Name())
        file, err := os.Open(filePath)
        if err != nil {
            fmt.Println("Error opening file:", err)
            continue
        }

        hash := hashFunc()
        if _, err := io.Copy(hash, file); err != nil {
            fmt.Println("Error reading file:", err)
            file.Close()
            continue
        }
        file.Close()

        sum := fmt.Sprintf("%x", hash.Sum(nil))
        if existingFile, found := hashes[sum]; found {
            err := os.Remove(filePath)
            if err != nil {
                fmt.Println("Error deleting file:", err)
            } else {
                fmt.Printf("Deleted duplicate file: %s (same as %s, hash: %s%s)\n", filePath, existingFile, hashFuncName, sum)
            }
        } else {
            hashes[sum] = filePath
        }
    }
}
```

### 手動で修正
出力をすっきりするようファイルパスからファイル名に変更

```go
fmt.Printf("Deleted duplicate file: %s (same as %s %s:%s)\n", fileName, existingFile, hashFuncName, sum)

```

## 画像左上の色でフォルダ分け

### プロンプト
>次は画像左上の色を取得して#00181Dと#002229で2つのフォルダを分けて画像ファイルを移動するプログラムを作成してください。画像ではないor指定した色ではない場合移動させないでください。プログラムへの入力はディレクトリパスとします。

### 出力されたプログラム
```go
package main

import (
    "fmt"
    "image"
    _ "image/jpeg"
    _ "image/png"
    "os"
    "path/filepath"

    "github.com/disintegration/imaging"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: go run main.go <directory_path>")
        return
    }

    dir := os.Args[1]
    files, err := os.ReadDir(dir)
    if err != nil {
        fmt.Println("Error reading directory:", err)
        return
    }

    // Create target directories if they do not exist
    dir00181D := filepath.Join(dir, "#00181D")
    dir002229 := filepath.Join(dir, "#002229")
    os.MkdirAll(dir00181D, os.ModePerm)
    os.MkdirAll(dir002229, os.ModePerm)

    for _, file := range files {
        if file.IsDir() {
            continue
        }

        filePath := filepath.Join(dir, file.Name())
        img, err := imaging.Open(filePath)
        if err != nil {
            fmt.Printf("Error opening image file %s: %v\n", filePath, err)
            continue
        }

        // Get the color of the top-left pixel
        topLeftColor := img.At(0, 0)

        // Compare the color with the specified colors
        r, g, b, _ := topLeftColor.RGBA()
        colorHex := fmt.Sprintf("#%02X%02X%02X", r>>8, g>>8, b>>8)

        switch colorHex {
        case "#00181D":
            newPath := filepath.Join(dir00181D, file.Name())
            err = os.Rename(filePath, newPath)
            if err != nil {
                fmt.Printf("Error moving file %s to %s: %v\n", filePath, newPath, err)
            } else {
                fmt.Printf("Moved %s to %s\n", filePath, newPath)
            }
        case "#002229":
            newPath := filepath.Join(dir002229, file.Name())
            err = os.Rename(filePath, newPath)
            if err != nil {
                fmt.Printf("Error moving file %s to %s: %v\n", filePath, newPath, err)
            } else {
                fmt.Printf("Moved %s to %s\n", filePath, newPath)
            }
        default:
            fmt.Printf("File %s does not match the specified colors\n", filePath)
        }
    }
}
```

### 修正プロンプト
>プログラムを変更して画像左上の色ごとにフォルダを作成して移動させるプログラムに変更してください

### 修正されたプログラム
```go
package main

import (
    "fmt"
    "image"
    _ "image/jpeg"
    _ "image/png"
    "os"
    "path/filepath"

    "github.com/disintegration/imaging"
)

func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: go run main.go <directory_path>")
        return
    }

    dir := os.Args[1]
    files, err := os.ReadDir(dir)
    if err != nil {
        fmt.Println("Error reading directory:", err)
        return
    }

    for _, file := range files {
        if file.IsDir() {
            continue
        }

        filePath := filepath.Join(dir, file.Name())
        img, err := imaging.Open(filePath)
        if err != nil {
            fmt.Printf("Error opening image file %s: %v\n", filePath, err)
            continue
        }

        // Get the color of the top-left pixel
        topLeftColor := img.At(0, 0)

        // Convert the color to a hex string
        r, g, b, _ := topLeftColor.RGBA()
        colorHex := fmt.Sprintf("#%02X%02X%02X", r>>8, g>>8, b>>8)

        // Create directory for the color if it does not exist
        colorDir := filepath.Join(dir, colorHex)
        err = os.MkdirAll(colorDir, os.ModePerm)
        if err != nil {
            fmt.Printf("Error creating directory %s: %v\n", colorDir, err)
            continue
        }

        // Move the file to the corresponding directory
        newPath := filepath.Join(colorDir, file.Name())
        err = os.Rename(filePath, newPath)
        if err != nil {
            fmt.Printf("Error moving file %s to %s: %v\n", filePath, newPath, err)
        } else {
            fmt.Printf("Moved %s to %s\n", filePath, newPath)
        }
    }
}
```

### 手動で修正

出力をすっきりするようファイルパスからファイル名に変更
```go
// Move the file to the corresponding directory
fName := file.Name()
newPath := filepath.Join(colorDir, fName)
err = os.Rename(filePath, newPath)
if err != nil {
	fmt.Printf("Error moving file %s to %s: %v\n", fName, colorHex, err)
} else {
	fmt.Printf("Moved %s to %s\n", fName, colorHex)
}
  ```

## 感想
補完機能だけしか使っていなかったが簡単なツール作成に数時間使うぐらいなら月に2-3個簡単なツールを作るだけで10ドルの元が取れた

あとは無駄に凝ってしまう変数名を考えるのにも使えるし、英語力が弱いので英語ドキュメントを読むのに翻訳とかコーディングに関係ない時間を減らして生産性を上げられたらと思う