C++による二次元配列を用いた点字出力とファイルへの単語保存


# はじめに
こんにちは、カナダにコンピューターサイエンス留学中の大学1年生です。
期末試験（Final）が終わり、余裕ができたのでC++の授業の課題の一つについて備忘録も兼ねてダラダラと書いていこうと思います。
初めてQiitaを書くので違和感あるかもしれませんがご容赦ください。

長くなるかもしれませんが、どこかのどなたかの少しでも参考になれば幸いです。CS留学を考えている方にはこういうことをやるよ、みたいな一つの参考にはなるかも…。
こうした方がいいよ、やここは違うのでは？などのご意見もあればよろしくお願いします。


　
## 目次

[1. 前提](#1-前提)
[2. 要件](#2-要件)
[3. プログラム概要](#3-プログラム概要)
[4.おわりに](#4-おわりに)

　

## 1. 前提
今回のC++の授業はプログラミング言語を触れたことがない人に向けての授業でした。（僕自身もプログラミング初心者です。）そのためこの課題は大学の授業等でC++の知識が多少あればできる内容になっているかとは思います。以下、使用した知識の一部です。

今回書く課題は学期内4つあったうちの最後の課題です。
今回の講義ではポインタは扱いませんでしたが、参照は扱いましたので今回の課題でも使用しています。

- for, if文
- 関数
- 配列（二次元配列）
- 参照
- ファイル出入力

平均点は分かりませんが、自分の今回のプログラムは 95 / 100 でした。


以下、今回の課題の出入力の完成形です。
```terminal:terminal
Enter a sentence: Hi Leo
o.
oo
..
------
.o
o.
..
------
..
..
..
------
o.
o.
o.
------
o.
.o
..
------
o.
.o
o.
------

Would you like to play again Y/y for yes and N/n for no?
n
```
　

　

## 2. 要件
以下、今回のプログラムの要件です。


>各関数は1つのタスクだけを実行する必要があり、点字テキストコーディングとして実行されます。プログラムは、dictionary.txt というテキストファイルから単語のリストを読み込む必要があります。また、4文字から6文字の長さのすべての単語を、dictionaryというベクトルに保存する必要があります。
>　
>1. ユーザーに文章を入力させる。
>2. 文中のすべての単語が辞書のベクトル内にあるかどうかをチェックする。辞書ファイルに存在しない単語をベクトルに追加する。プログラムの末尾に、辞書ベクトルをdictionary.txtに保存する。
>3. 大文字に変換された文章を点字に印刷する。点字は2次元配列で保存する。
>
>このプログラムでは、ユーザーが大文字または小文字を使ってY/Nの質問に答えることで、繰り返しプログラムを実行できるようにする必要があります。
>
>プログラムには、次のような機能を持たせること：
>1- ユーザーに文章を入力させる関数。
>2- テキストファイルから単語を読み込み、ベクタルに保存する機能。
>3- 文章を点字に変換する関数。
>4- 辞書ベクトルに新しい単語を追加する関数。
>5- 辞書ベクトルを dictionary.txt に保存する機能。
>6- プログラムを完了するための追加機能
>
>上記の関数を呼び出してプログラムを制御するメイン関数を追加。

　

以下、実際の課題のスクショです。
![Screenshot 2023-04-20 at 3.25.06 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1998455/3bdb5cb8-03c7-1e62-3745-ee433852057c.png)
![Screenshot 2023-04-20 at 3.25.13 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1998455/f6a570ab-5dc9-bb06-3cd7-9a0a6389651e.png)



　



## 3. プログラム概要

[1. 二次元配列による点字の保存と出力](#1-二次元配列による点字の保存と出力)
[2. 英文の入力](#2-英文の入力)
[3. 単語の抽出](#3-単語の抽出)
[4. 辞書ファイルの読み込み](#4-辞書ファイルの読み込み)
[5. 辞書の中から単語検索](#5-辞書の中から単語検索)
[6. 辞書ベクトルに単語を追加](#6-辞書ベクトルに単語を追加)
[7. 辞書ファイルへの書き込み](#7-辞書ファイルへの書き込み)

:::note warn
ヘッダーファイル、プロトタイプは省きます。
:::
 
 
　


　
### 1. 二次元配列による点字の保存と出力
今回のプログラムの肝であり、自分の中で地味に苦労した部分です。そのためプログラム概要の冒頭に書かせていただきました。
授業で二次元配列について詳しく習っていなかったので課題を出された次の授業で理解できました。



#### 1.1 点字の保存
今回はアルファベット26文字と空白、句読点（全て'.'）のみの点字を保存してあります。
点字1文字分が縦３、横２列ですので二次元配列はBraille[3][(26 + 1) * 2]となります。（今回のプログラムは'!'や'?'などの特殊記号には対応していません）


```cpp:cpp
const int ROWS = 3; // 縦列
const int COLS = 54; // 横列

// 点字の配列
char Braille[ROWS][COLS] = {
    { 'o', '.',   'o', '.',   'o', 'o',   'o', 'o',   'o', '.',   'o', 'o',   'o', 'o',   'o', '.',   '.', 'o',   '.', 'o',   'o', '.',   'o', '.',   'o','o',  'o', 'o',   'o', '.',   'o', 'o',   'o', 'o',   'o', '.',   '.', 'o',   '.', 'o',   'o', '.',   'o', '.',   '.', 'o',   'o', 'o',   'o', 'o',   'o', '.',   '.', '.' }, // Row 1
    { '.', '.',   'o', '.',   '.', '.',   '.', 'o',   '.', 'o',   'o', '.',   'o', 'o',   'o', 'o',   'o', '.',   'o', 'o',   '.', '.',   'o', '.',   '.','.',  '.', 'o',   '.', 'o',   'o', '.',   'o', 'o',   'o', 'o',   'o', '.',   'o', 'o',   '.', '.',   'o', '.',   'o', 'o',   '.', '.',   '.', 'o',   '.', 'o',   '.', '.' }, //　Row 2
    { '.', '.',   '.', '.',   '.', '.',   '.', '.',   '.', '.',   '.', '.',   '.', '.',   '.', '.',   '.', '.',   '.', '.',   'o', '.',   'o', '.',   'o','.',  'o', '.',   'o', '.',   'o', '.',   'o', '.',   'o', '.',   'o', '.',   'o', '.',   'o', 'o',   'o', 'o',   '.', 'o',   'o', 'o',   'o', 'o',   'o', 'o',   '.', '.' } // Row 3
};
```
[点字参考](https://www.pharmabraille.com/pharmaceutical-braille/the-braille-alphabet/)

　

　

#### 1.2 点字の呼び出し
この課題では前述した二次元配列に保存した点字を1文字ごとに出力する必要があります。
点字呼び出しの関数では戻り値はないため変数型はvoidです。

1文字ごとの点字の場所を特定するためにはfor文を用います。
index変数に引数 c から 'A' の文字コードを引いて格納します。ここでいう文字コードは、ASBIIコードを使用しており、大文字の 'A' は 65 に対応しています。これにより、引数cが何番目のアルファベットであるかを求めることができます。
　

:::note
<font color="##78C841">**Example!**</font>
例えば、cがFの時、FのASCIIコードは70なので c - 'A' つまり 70 - 65でFは5番目の文字であることがわかります。

その後for文により、縦3回、横2回分の配列を出力することでアルファベット点字1文字分の出力を完了させることができます。
:::
　

スペースは、二次元配列の最後に保存してあるため、for文でBraille[i][COLS - 2],Braille[i][COLS - 1]、つまりBraille[i][53]とBraille[i][54]を（3行分）出力します。
　


```cpp:cpp
void printBraille(char c) {
    int index = 0;
    c = toupper(c);   //　小文字を大文字に変換
    if (c >= 'A' && c <= 'Z') {   // アルファベット
        index = c - 'A';
        for (int i = 0; i < ROWS; i++) {
            for (int j = index * 2; j < (index + 1) * 2; j++) {
                cout << Braille[i][j];
            }
            cout << endl;
            
        }
        cout << "------" << endl;
    }
    else if (c == ' ') { // スペース
        for (int i = 0; i < ROWS; i++) {
            cout << Braille[i][COLS - 2] << Braille[i][COLS - 1] << endl;
        }
        cout << "------" << endl; 
    }
}
```

今回のプログラムでは点字出力は点字1文字ごとに改行される仕様になっているのでわかりやすくするために点字1文字の出力終了後に"-----"で区切るようにしてあります。
　

今回のプログラムでは僕は点字の配列や縦列横列定数をグローバル変数として定義したのですが、これが今回の減点の原因だったようです。（確かにあまり使わないでって言ってたかも…。）
![Screenshot 2023-04-20 at 5.21.06 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1998455/75d8b099-3911-1323-70df-647fc7f9ea22.png)



　



### 2. 英文の入力
このプログラムでは「単語」ではなく「文章」の入力なのでgetline関数を使用して行ごと読み込みます。これにより空白や句読点も読み込むことができます。
入力された文章をstringとして返すので変数型はstring、後述するプログラムで入力した文章を使用するためにreturn sentenceを記述します。

```cpp:cpp
string getUserInput() {
    string sentence;
    cout << "Enter a sentence: ";
    getline(cin, sentence);  // getlineによる文章の入力
    return sentence;
}
```
　
　


### 3. 単語の抽出
この関数では入力された英文を単語ごとに分け、４〜６文字の単語のみを返します。（min, maxについてはmain関数内で定義しました。）
動きは以下のとおりです。


> - [x] 空のベクトル words を用意し、文字列を空の文字列 words として初期化する。
> - [x] 入力された文字列sentence内の各文字cについて以下を繰り返します。
    1. もしcが空白、カンマ、またはピリオドの場合、現在のwordの長さが指定された範囲内(４〜６文字)である場合、wordsにwordを追加。
    2. wordを空の文字列に初期化する。
    3. そうでなければ、cをwordに追加する。
> - [x] 最後のwordをチェックし、その長さが指定された範囲内 (４〜６文字) である場合、wordsに追加する
> - [x] words を返す

```cpp:cpp
vector<string> extractWordsFromSentence(const string& sentence, int minLen, int maxLen) {
    vector<string> words;
    string word;
    for (const auto& c : sentence) {
        if (c == ' ' || c == ',' || c == '.') {
            if (word.length() >= minLen && word.length() <= maxLen) {
                words.push_back(word);
            }
            word.clear();
        }
        else {
            word += c;
        }
    }

    // 最後の単語の長さを確認
    if (word.length() >= minLen && word.length() <= maxLen) {
        words.push_back(word);
    }

    return words;
}

```

:::note
<font color="##78C841">**Example!**</font>
例えば、"Leo likes coding cpp."という文章が与えられ、文字数が３〜５文字と指定された場合、この関数は以下のように動作します。

> - [x] wordsは空のvector、wordは空の文字列。
> - [x] 文字列の最初の文字'L'がwordに追加される。
> - [x] 文字列の2番目の文字'e'がwordに追加される。
> - [x] 文字列の3番目の文字'o'がwordに追加される。
> - [x] 文字列の4番目の文字' 'が検出されるため、現在のwordは"leo"となり、長さは3で指定された範囲内にあるため、wordsに追加される。
>
> - [x] wordは空の文字列に初期化される。
> - [x] 文字列の5番目の文字'l'がwordに追加される。
> - [x] 文字列の6番目の文字'i'がwordに追加される。
> - [x] 文字列の7番目の文字'k'がwordに追加される。
> - [x] 文字列の8番目の文字'e'がwordに追加される。
> - [x] 文字列の9番目の文字's'がwordに追加される。
> - [x] 現在のwordは"likes"となり、長さは指定された範囲内にあるため、wordsに追加される。
>
> - [x] wordは空の文字列に初期化される。
> - [x] 以下同様に繰り返し、"coding"は6文字であるため、スキップされ、最後に"cpp"がwordsに追加されて返される。
:::


　
　



### 4. 辞書ファイルの読み込み
この関数では辞書ファイルを読み込み、辞書ベクトルに辞書単語を追加します。
動きは以下の通りです。

> - [x] ファイルストリームを使用して辞書ファイルを読み込みます。
> - [x] 辞書ファイルを一行ずつ読み込みlineに格納する
> - [x] lineをdictionaryに追加する
> - [x] dictionaryを返す


```cpp:cpp
vector<string> readDictionaryFromFile(const string& filename)
{
    vector<string> dictionary;
    ifstream inFile(filename);
    if (inFile.is_open()) {
        string line;
        while (getline(inFile, line)) {
            dictionary.push_back(line);
        }
        inFile.close();
    }
    return dictionary;
}
```

　
　
　


### 5. 辞書の中から単語検索
この関数ではセクション４で文章から抽出した単語毎に辞書の中にそれらの単語が含まれているかどうかを確認し、boolean型で判別します。

for文を用いて、辞書ベクトルの中に先ほど抽出した単語毎に辞書ベクトルに含まれる単語と一致する場合はtrue,そうでないならfalseを返します。

```cpp:cpp
bool isWordInDictionary(const vector<string>& dictionary, const string& word)
{
    for (const auto& dictWord : dictionary) {
        if (dictWord == word) {
            return true;
        }
    }
    return false;
}
```
　


:::note
<font color="##78C841">**Example!**</font>
例えば辞書ベクトルが以下の場合、

```cpp:cpp
dictionary = {"apple", "banana", "cherry"}
word = "banana"
```
> - [x] 関数はdictionaryの最初の要素"apple"とwordを比較する。一致しないため、次の要素に移る。
> - [x] 次の要素"banana"が wordと一致するためtrueを返す

これをfor文によって繰り返します。
:::

　

### 6. 辞書ベクトルに単語を追加
セクション５で確認し、辞書の中に含まれていなかった単語を辞書ベクトルに追加します。

```cpp:cpp
void addWordToDictionary(vector<string>& dictionary, const string& word)
{
    dictionary.push_back(word);
}
```

　
　

### 7. 辞書ファイルへの書き込み
この関数ではセクション６で辞書ベクトルに追加した単語をファイルストリームを使用して辞書ファイルに書き込みます。

具体的にはstring filenameとvector<string> dictionaryを引数に受け取り、filenameで指定されたファイルを開き、ファイルの書き込み可能性を確認します。その後単語リストの各単語に対して、1行ずつファイルに書き込み、ファイルを閉じます。

```cpp:cpp
void saveDictionaryToFile(const string& filename, const vector<string>& dictionary)
{
    ofstream outFile(filename);
    if (outFile.is_open()) {
        for (const auto& word : dictionary) {
            outFile << word << endl;
        }
        outFile.close();
    }
}
```

　
　
　
### 8. メイン関数
課題要件のうちのY/Nの大文字、または小文字によりプログラムの繰り返しを選択させるためにdo-whileを使用します。do-whileによりループ内のプログラムを一度は実行することができます。

各関数の詳細は割愛します。

点字出力は入力された文章をそのまま点字文章に変換します。


:::note
<font color="##78C841">**Point!**</font>
cin.ignore()は、前回の入力操作の残りの改行文字をクリアし、次の入力操作が正しく処理されるようにするために必要です。cin.ignore()がない場合、do-whileでY/yを選択し、プログラムを再び実行する際にユーザーが文章を入力することができなくなります。
:::

```cpp:cpp
int main() {
    char option;
    string sentence;
    do {
        sentence = getUserInput();

        // 単語の抽出
        vector<string> words = extractWordsFromSentence(sentence, 4, 6);

        // 辞書ファイルから読み込み
        vector<string> dictionary = readDictionaryFromFile("dictionary.txt");

        // 辞書ベクトル、辞書ファイルに単語を追加
        for (const auto& word : words) {
            if (!isWordInDictionary(dictionary, word)) {
                addWordToDictionary(dictionary, word);
                saveDictionaryToFile("dictionary.txt", dictionary);
            }
        }

        cout << "Convert the sentence to Braille character" << endl;
        // 点字を出力
        for (int i = 0; i < sentence.length(); i++) {
            printBraille(sentence[i]);
        }

        cout << endl;
        // 繰り返しをY/y or N/nで選択
        cout << "Would you like to play again Y/y for yes and N/n for no?" << endl;
        cin >> option;
        cin.ignore(); // getlineでの入力をクリア
    } while (option == 'y' || option == 'Y');
    return 0;
}
```





　
　

## 4. おわりに
いかがでしたでしょうか？
僕自身、プログラミングを本格的に勉強するのかこの授業が初めてかつ英語だったのでなかなか苦労しました。

今回のプログラムは多少ChatCPTさんのお力添えがあってできたものなので完全に自分で考えたのものというわけではありませんが理解しながらコーディングできたので自分の中では良しとします😂（ちなみにChatGPTに限らずですがPlagiarism(盗作）疑いで呼び出されていた人もいたので使う際は賢く使いましょうw）

　

一番最初の授業で、先生がC++を覚えれば基本的にはどんなプログラムも書けるようになると言っていたのですが、最初のうちはC++はとても難しいというイメージやWebでの口コミを見てビビっていましたし、C++の難易度が高すぎて他の言語が簡単に感じるからなのかな？と思っていました。（今回はポインタや構造体は詳しくはやらなかったのもついていけた理由の一つかとは思います。）

学期が終了し、C++の大枠を学習したわけですが今では先生が最初に言っていたことがなんとなくわかるな、という感じです。
次の学期にとる授業がJavaの講義なので[Progate](https://prog-8.com/)や[Recursion](https://recursionist.io/)で軽く触ってみたのですがC++の基礎を学んだおかげか、スラスラと頭に入ってきます。Javaの授業ではOOPをやるようですが、今後もC++でJavaでやったことを復習したりなど継続できれば勉強に苦手意識のある自分にとっては大きな進歩ですw

初心者がC/C++を学ぶのは挫折する可能性が高いからやめた方がいいと言う意見もあり、僕も否定はしませんが素人感覚でCSを学ぶにあたってプログラミングを理解するのにはありなのではないかなと思いました。（独学の場合は他の言語を選んだ方がいい気がします😅）

　
長くなりましたが、最後まで読んでいただきありがとうございました！いいね頂けると喜びます😂
今後も備忘録やアウトプット程度に気ままに書いていけたらいいなと思います。