[C++] 319 BBS閲覧アプリの製作 その5 スレッドタイトルの取得 libxmlでパース

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

libxmlを使ったスクレイピングに成功しました。

ただしPythonのBeautifulSoup & パーサー(html.parserなど)によるものとは違い、文字列比較で絞り込んでいく原始的な手法です。

今回の方法ではUTF-8への変換は不要でした。

IDやXPathを指定してパースできるのか、今後調査します。

#include <iostream>
#include <string>
#include <vector>
#include <curl/curl.h>
#include <libxml/HTMLparser.h>

std::vector<std::pair<std::string, std::string>> idTitlePairs;

std::string convertToString(const xmlChar* xmlString) {
    if (xmlString == nullptr) {
        return "";
    }
    return std::string(reinterpret_cast<const char*>(xmlString));
}

void parseAnchorTags(xmlNode* node) {
    for (xmlNode* cur = node; cur; cur = cur->next) {
        if (cur->type == XML_ELEMENT_NODE && xmlStrcmp(cur->name, (const xmlChar*)"small") == 0) {
            xmlChar* id = xmlGetProp(cur, (const xmlChar*)"id");
            if (xmlStrcmp(id, (const xmlChar*)"trad") == 0) {
                for (xmlNode* child = cur->children; child; child = child->next) {
                    if (child->type == XML_ELEMENT_NODE && xmlStrcmp(child->name, (const xmlChar*)"a") == 0) {
                        xmlChar* id0 = xmlGetProp(child, (const xmlChar*)"href");
                        xmlChar* title0 = xmlNodeListGetString(child->doc, child->children, 1);
                        // std::cout << "id: " << id0 << std::endl;
                        // std::cout << "title: " << title0 << std::endl;

                        string id = convertToString(id0);
                        id.erase(id.size() - 4);
                        string title = convertToString(title0);

                        idTitlePairs.push_back(std::make_pair(id, title));

                        xmlFree(id0);
                        xmlFree(title0);
                    }
                }
            }
            xmlFree(id);
        }
        parseAnchorTags(cur->children);
    }
}

// コールバック関数
size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* buffer) {
    size_t totalSize = size * nmemb;
    buffer->append((char*)contents, totalSize);
    return totalSize;
}

int main() {
    // URLからHTMLファイルを取り込む
    std::string url = "HTMLファイルのURL";
    std::string htmlBuffer;
    CURL* curl = curl_easy_init();
    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &htmlBuffer);
        CURLcode res = curl_easy_perform(curl);
        curl_easy_cleanup(curl);
        if (res != CURLE_OK) {
            std::cerr << "Error: Failed to download HTML" << std::endl;
            return 1;
        }
    } else {
        std::cerr << "Error: Failed to initialize CURL" << std::endl;
        return 1;
    }

    // htmlBufferの確認
    // cout << "htmlBuffer: \n" << htmlBuffer << endl;

    xmlDoc* doc = htmlReadMemory(htmlBuffer.c_str(), htmlBuffer.size(), nullptr, nullptr, HTML_PARSE_RECOVER);
    if (doc) {
        xmlNode* root = xmlDocGetRootElement(doc);
        parseAnchorTags(root);
        xmlFreeDoc(doc);
    }

    for (const auto& pair : idTitlePairs) {
        std::cout << "id: " << pair.first << std::endl;
        std::cout << "title: " << pair.second << std::endl;
    }

    return 0;
}

[C++] 318 BBS閲覧アプリの製作 その4 スレッドタイトルの取得 HTMLパーサーを使わず文字列処理

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

tidy-html5やlibxml2といった有名どころのHTMLパーサーを使おうとしたものの、上手く使いこなせません。

文字列加工関数や正規表現を使ってスレッドIDとスレッドタイトルを2次元vectorにまとめました。

#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <curl/curl.h>
#include <iconv.h>

// コールバック関数
size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* buffer) {
    size_t totalSize = size * nmemb;
    buffer->append((char*)contents, totalSize);
    return totalSize;
}

std::string ConvertShiftJISToUTF8(const std::string& input) {
    std::string output;

    iconv_t cd = iconv_open("UTF-8", "CP932");
    if (cd == (iconv_t)-1) {
        std::cerr << "Error: Failed to open iconv" << std::endl;
        return output;
    }

    size_t inBytes = input.size();
    size_t outBytes = inBytes * 4; // 変換後の最大バイト数を予測して確保する

    char* inBuf = const_cast<char*>(input.c_str());
    char* outBuf = new char[outBytes];
    char* outPtr = outBuf;

    if (iconv(cd, &inBuf, &inBytes, &outPtr, &outBytes) == (size_t)-1) {
        std::cerr << "Error: Failed to convert encoding: " << strerror(errno) << std::endl;
        delete[] outBuf;
        iconv_close(cd);
        return output;
    }

    output.assign(outBuf, outPtr - outBuf);

    delete[] outBuf;
    iconv_close(cd);

    return output;
}

int main() {
    // URLからHTMLファイルを取り込む
    std::string url = "HTMLファイルのURL";
    std::string htmlBuffer;
    CURL* curl = curl_easy_init();
    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &htmlBuffer);
        CURLcode res = curl_easy_perform(curl);
        curl_easy_cleanup(curl);
        if (res != CURLE_OK) {
            std::cerr << "Error: Failed to download HTML" << std::endl;
            return 1;
        }
    } else {
        std::cerr << "Error: Failed to initialize CURL" << std::endl;
        return 1;
    }

    // 文字コードをCP932(Microsoftの拡張Shift-JIS)からUTF-8へ変換
    htmlBuffer = ConvertShiftJISToUTF8(htmlBuffer);

    // htmlBufferの確認
    // cout << "htmlBuffer: \n" << htmlBuffer << endl;

    // smallタグ部分を抽出
    std::string delimiter = "<small id=\"trad\">";
    htmlBuffer = htmlBuffer.substr(htmlBuffer.find(delimiter) + delimiter.length());
    delimiter = "</small>";
    htmlBuffer = htmlBuffer.substr(0, htmlBuffer.find(delimiter));

    // std::cout << "抽出内容:\n" << htmlBuffer << std::endl;

    // 正規表現を使ってaタグ内のhrefとaタグの内容を抽出 (.*?)部分
    std::regex pattern("<a.*?href=\"(.*?)/l50\".*?>(.*?)</a>");
    std::smatch matches;

    std::string::const_iterator searchStart(htmlBuffer.cbegin());
    std::vector<std::pair<std::string, std::string>> idTitlePairs;
    while (std::regex_search(searchStart, htmlBuffer.cend(), matches, pattern)) {
        std::string id = matches[1];
        std::string title = matches[2];
        idTitlePairs.push_back(std::make_pair(id, title));
        searchStart = matches.suffix().first;
    }

    for (const auto& pair : idTitlePairs) {
        std::cout << "id: " << pair.first << std::endl;
        std::cout << "title: " << pair.second << std::endl;
    }

    return 0;
}

[C++] 317 BBS閲覧アプリの製作 その3 スレッドタイトルの取得 curl

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

まずcurlライブラリを使ってHTMLファイルを取り込みます。

文字コードがシフトJISであってもMicrosoftの拡張シフトJISの場合はCP932を使わないと文字化けを起こします。

#include <curl/curl.h>
#include <iconv.h>

std::string ConvertShiftJISToUTF8(const std::string& input) {
    std::string output;

    iconv_t cd = iconv_open("UTF-8", "CP932"); // SHIFT-JISではエラーになった
    if (cd == (iconv_t)-1) {
        std::cerr << "Error: Failed to open iconv" << std::endl;
        return output;
    }

    size_t inBytes = input.size();
    size_t outBytes = inBytes * 4; // 変換後の最大バイト数を予測して確保する

    char* inBuf = const_cast<char*>(input.c_str());
    char* outBuf = new char[outBytes];
    char* outPtr = outBuf;

    if (iconv(cd, &inBuf, &inBytes, &outPtr, &outBytes) == (size_t)-1) {
        std::cerr << "Error: Failed to convert encoding: " << strerror(errno) << std::endl;
        delete[] outBuf;
        iconv_close(cd);
        return output;
    }

    output.assign(outBuf, outPtr - outBuf);

    delete[] outBuf;
    iconv_close(cd);

    return output;
}

int main() {
    // URLからHTMLファイルを取り込む
    std::string url = "HTMLファイルのURL";
    std::string htmlBuffer;
    CURL* curl = curl_easy_init();
    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &htmlBuffer);
        CURLcode res = curl_easy_perform(curl);
        curl_easy_cleanup(curl);
        if (res != CURLE_OK) {
            std::cerr << "Error: Failed to download HTML" << std::endl;
            return 1;
        }
    } else {
        std::cerr << "Error: Failed to initialize CURL" << std::endl;
        return 1;
    }

    // 文字コードをCP932(Microsoftの拡張Shift-JIS)からUTF-8へ変換
    htmlBuffer = ConvertShiftJISToUTF8(htmlBuffer);

    // htmlBufferの確認
    cout << "htmlBuffer: \n" << htmlBuffer << endl;

<以下略>

[C++,Python] 316 BBS閲覧アプリの製作 その2 スレッドタイトルの取得 Python編

[M1 Mac, MacOS Ventura 13.3.1, Python 3.10.4]

スレッドタイトルとスレッドIDを取得するPythonスクリプトを書きました。

次はこのスクリプトをC++へ変換したいです。難しくなるようであればモジュール化します。

機能はPythonで下書きしてC++へ変換あるいはモジュール化、GUIはFLTK(C++)でコーディングしていきます。

import requests
import re
from bs4 import BeautifulSoup

# URLからHTMLファイルを取り込む
url = 'スレッドタイトル表示URL(HTMLファイル)'
response = requests.get(url)
response.encoding = response.apparent_encoding
html = response.text

# BeautifulSoupを使用してHTMLを解析する
soup = BeautifulSoup(html, 'html.parser')

# <small id="trad">タグで囲まれた部分の内容を取得する
trad_tags = soup.find_all('small', id='trad')

thread_list = []
for trad_tag in trad_tags:
    content = trad_tag.get_text()

    # <a>タグのhref属性の値を正規表現で取得する
    pattern = r'<a\s+href=[\'"]([^\'"]+)[\'"]'
    href = re.findall(pattern, str(trad_tag))

    # contentをaタグごとに分割してリストにする
    content_list = re.split(r'<a\s+href=[\'"][^\'"]+[\'"]', str(trad_tag))
    
    print(f"hrefの要素数: {len(href)}")
    print(f"content_listの要素数: {len(content_list)}")

    # 辞書型データのリストを作成する
    for i in range(len(href)):
        thread_list.append({'href': (href[i])[:-4], 'content': (content_list[i+1].strip().replace("</a>","").replace("</small>",""))[1:]})

# リストの内容を出力する
for thread in thread_list:
    print(thread['href'])
    print(thread['content'])

[C++,Python] 315 BBS閲覧アプリの製作 その1 DATファイルの保存

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

とあるBBSのDATファイルへのアクセスが可能になったようなので、早速遊んでみることにしました。

とりあえずDATファイルをダウンロードしてみます。PythonスクリプトをChatGPTに変換してもらったコードがそのまま使えました。

DATファイルの文字コードがシフトJISですから、Macの場合はUTF-8に変換する必要がありますね。

#include <iostream>
#include <fstream>
#include <curl/curl.h>

size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
    std::ofstream* file = static_cast<std::ofstream*>(userp);
    file->write(static_cast<char*>(contents), size * nmemb);
    return size * nmemb;
}

int main() {
    std::string url = "DATファイルのurl";
    std::string filename = "保存先DATファイルのパス";

    CURL* curl = curl_easy_init();
    if (curl) {
        std::ofstream file(filename, std::ios::binary);
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file);
        CURLcode res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            std::cerr << "Error: " << curl_easy_strerror(res) << std::endl;
        }
        curl_easy_cleanup(curl);
    } else {
        std::cerr << "Failed to initialize curl" << std::endl;
    }

    return 0;
}

[Swift] 32 ChatGPTアプリ製作 その3 指示文のソート

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

指示文が連続する会話形式ではデータをJSONファイルにするなど保存するデータがややこしくなるため、一問一答形式でブラッシュアップしました。

指示文が作成順にうまく並ばないので、EntityのAttributeに生成日時を追加しこれをキーとしてソートさせています。常に先頭の指示文を送信する仕様になっています。

また、指示済かつ回答受信済の内容については送信ボタンを押すとアラートが表示されます。

保存するデータがEntityの形式に縛られるのが何とももどかしいです。JSONファイルとしてiCloudやローカルに出力できないか調べてみます。

これで土台はできあがったので、後はのんびり進めていきます。気が向いたら会話形式にも着手します。

アラート表示
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        entity: Interaction.entity(),
        sortDescriptors: [NSSortDescriptor(key: "creationDate", ascending: false)])
    private var pairs: FetchedResults<Interaction>

    @State var isShowAlert = false

    var body: some View {
        NavigationView {
            VStack{
                List {
                    ForEach(pairs) { pair in
                        NavigationLink{
                            if((pair.instruction?.isEmpty) == false){
                                Draft(text:pair.instruction!, interaction: pair)
                            }
                        }
                        label:{
                            if((pair.instruction?.isEmpty) == false){
                                Text(pair.instruction!)
                            }
                        }
                    }

                    .onDelete(perform: deleteInteraction)
                }
                .navigationTitle("ChatGPTSwift")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar{
                    // 新規作成
                    ToolbarItem(placement:.navigationBarTrailing){
                        NavigationLink{
                            Draft()
                        }label:{
                            Text("+")
                                .font(.system(size: 30))
                        }
                    }
                }

                List {
                    ForEach(pairs, id: \.self) { pair in
                        if let res = pair.res , !res.isEmpty {
                            Text(res)
                            .foregroundColor(.white)
                            .background(Color.blue)
                        }
                    }


                    .onDelete(perform: deleteInteraction)
                }

                Button(action:{
                    if pairs.first != nil{
                        let instruction = pairs.first
                        
                        if instruction!.res == nil{
                            sendRequest(pairs:pairs)
                        }else{
                            isShowAlert = true
                            print("指示文を入力して下さい")
                        }
                        
                    }else{
                        print("pairsは空です")
                        
                    }
                }){
                    Text("送信")
                    .font(.system(size: 24))
                }
                .alert("指示文を入力して下さい", isPresented: $isShowAlert) {
                    Button("OK") {
                    }
                } message: {
                    Text("")
                }
            }
        }
    }
<以下略>
Core Data(Data Model)の内容
creationDateを追加

[Swift] 31 ChatGPTアプリ製作 その2 HTTPRequest

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

GPT-3.5のAPIとやりとりできるようになりました。

ChatGPTの力を借り色々試行錯誤しながらで約半日掛かりました。取りあえず送受信を1往復できるようにしています。

チャットのように交互に表示させるのは難しいため、上下段に振り分けました。

次回以降、複数回の送受信に対応させます。

OpenAIがiOS, iPadOSアプリを5/26にリリースしているので、比較しながら機能を増やしていきたいです。

iOS版
iPadOS版
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(sortDescriptors:[])
    private var pairs: FetchedResults<Interaction>

    var body: some View {
        NavigationView {
            VStack{
                List {
                    ForEach(pairs) { pair in
                        NavigationLink{
                            if((pair.instruction?.isEmpty) == false){
                                Draft(text:pair.instruction!, interaction: pair)
                            }
                        }
                        label:{
                            if((pair.instruction?.isEmpty) == false){
                                Text(pair.instruction!)
                            }
                        }
                    }

                    .onDelete(perform: deleteInteraction)
                }
                .navigationTitle("ChatGPT リクエスト")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar{
                    // 新規リクエスト作成
                    ToolbarItem(placement:.navigationBarTrailing){
                        NavigationLink{
                            Draft()
                        }label:{
                            Text("+")
                        }
                    }
                }

                List {
                    ForEach(pairs, id: \.self) { pair in
                        if let res = pair.res, !res.isEmpty {
                            Text(res)
                                .foregroundColor(.white)
                                .background(Color.blue)
                        }
                    }


                    .onDelete(perform: deleteInteraction)
                }

                Button(action:{
                    if pairs.last != nil{
                        sendRequest()
                    }else{
                        print("pairsは空です")
                        
                    }
                }){
                    Text("送信")
                    .font(.system(size: 24))
                }
            }
        }
    }

    func deleteInteraction(offsets:IndexSet){
        for offset in offsets{
            viewContext.delete(pairs[offset])
        }
            
        do{
            try viewContext.save()
        }catch{
            fatalError("セーブに失敗")
        }
    }

    func sendRequest(){
        let urlAPI = "https://api.openai.com/v1/chat/completions";
        let apiKey = "API key";
        let model = "gpt-3.5-turbo"
        let systemStr: String = "あなたは根拠が明確に存在することのみ発言するチャットボットです。"

        let authHeader = "Bearer \(apiKey)"
        var headers = [String: String]()
        headers["Authorization"] = authHeader
        headers["Content-Type"] = "application/json"

        guard let url = URL(string: urlAPI) else {
            fatalError("Invalid URL")
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.allHTTPHeaderFields = headers

        // requestData作成・送受信
        if let lastInteraction = pairs.last {
            if let instruction:String? = lastInteraction.instruction {
                print("instruction:")
                print(instruction)

                let requestData:String = "{\"model\":\"\(model)\", \"messages\":[{\"role\":\"system\",\"content\":\"\(systemStr)\"},{\"role\":\"user\",\"content\":\"\(instruction!)\"}], \"temperature\":0.0}";
                print(requestData)
                
                var responseData: [String: Any] = [:]
                responseData = sendHTTPRequest(url: url, headers: headers, requestData: requestData)
                
                print(responseData)

                if let choices = responseData["choices"] as? [[String: Any]],
                let message = choices.first?["message"] as? [String: Any],
                var content = message["content"] as? String {
                    print(content)
                    lastInteraction.res = content
                }

            } else {
                print("instruction is nil")
                return
            }
        } else {
            print("pairs is empty")
            return
        }
    }

    func sendHTTPRequest(url: URL, headers: [String: String], requestData: String) -> [String: Any] {
        var responseData: [String: Any] = [:]
        var timeoutBool = false

        let semaphore = DispatchSemaphore(value: 0)

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.allHTTPHeaderFields = headers
        request.httpBody = requestData.data(using: .utf8) // requestDataをData型に変換

        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                timeoutBool = true
                print("HTTP request failed: \(error)")
                return
            }

            if let data = data {
                do {
                    responseData = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
                } catch {
                    print("Failed to parse response data: \(error)")
                }
            }

            semaphore.signal()
        }

        task.resume()
        _ = semaphore.wait(timeout: DispatchTime.now() + 90)

        if timeoutBool {
            <中略>
            return [:]
        }

        return responseData
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Core Data(Data Model)の内容

[Swift] 29 ChatGPTアプリ製作 その1 クリップボード貼付

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

ChatGPTアプリiOS版の製作に着手しました。

取りあえずリクエスト文を入力できるようにしました。メモアプリのコードを一部流用しています。

クリップボードからテキストをペーストできます。macOSなどからOSをまたいでのペーストは次回以降対応するつもりです。

次にこれをAPIに送信してレスポンスを表示するようにします。

2023/7/4追記
TextEditorのタップ時にPasteやSelect Allなどが選択できるため、緑字のペーストボタンは削除しました。

アプリ画面
TextEditor画面
import SwiftUI

struct Draft: View {
    @State var text = ""
    @FocusState var nameFieldIsForcused: Bool
    @Environment(\.managedObjectContext)var viewContext
    var interaction: Interaction?

    var body: some View {
        TextEditor(text:$text)
            .frame(minHeight: 0, maxHeight: .infinity) // 行数無制限
            .focused($nameFieldIsForcused)
            .onAppear{
                DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+0.5){
                    nameFieldIsForcused = true
                }
            }
            .toolbar{
                ToolbarItem(placement:.navigationBarLeading){
                    if(nameFieldIsForcused){
                        Button(action:{
                            pasteText()

                        }){
                            Text("ペースト")
                            .foregroundColor(.green)
                        }
                    }
                }

                ToolbarItem(placement:.navigationBarTrailing){
                    if(nameFieldIsForcused){
                        Button(action:{
                            if interaction != nil{
                                updateContent(interaction: self.interaction!)
                            }else{
                                addContent()
                            }

                        }){
                            Text("完了")
                        }
                    }
                }
            }
    }

    func updateContent(interaction:Interaction){
        interaction.request = text
        
        do{
            try viewContext.save()
        }catch{
            fatalError("セーブに失敗")
        }

        nameFieldIsForcused = false
    }

    func addContent(){
        let newContent = Interaction(context:viewContext)
        newContent.request = text
        
        do{
            try viewContext.save()
            
        }catch{
            fatalError("セーブに失敗")
        }

        nameFieldIsForcused = false

    }

    func pasteText(){
        let pasteboard = UIPasteboard.general

        if let clipboardString = pasteboard.string {
            text += clipboardString
        }
    }
}

struct Draft_Previews: PreviewProvider {
    static var previews: some View {
        Draft()
    }
}

[AI] プログラミングLLM phi-1が公開予定 Microsoft

Microsoftが開発しているプログラミングに特化したLLM “phi-1″がHugging Faceに公開されるらしいです。

モデルサイズはGPT-3.5の100分の1以下、1.3Bです。能力はGPT-4には及ばないものの、GPT-3.5を少し上回っています(HumanEval比較)。

RTX 4070Tiで動かせるのか分かりませんが、今から待ちどおしいです。

GPT-3.5の100分の1以下のモデルサイズでより高いプログラミング処理能力持つ「phi-1」Microsoftが開発

[Swift] 26 Apple WatchのComplication改良 カレンダー / 元号を追加

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

改良カレンダーをタップした時の表示に元号を追加しました。フォントサイズを大きくすると赤色でも問題なく読めます。

今回は日本固有の内容だったためかChatGPTの回答がかなり怪しく、大幅な修正を加えました。

import SwiftUI

struct ContentView: View {
    let date: Date
    
    var body: some View {
        VStack (spacing: 15){
            Text(getFormattedDate())
            .font(.system(size: 30))
            .foregroundColor(.green)
            
            Text(getFormattedYear())
            .font(.system(size: 30))
            .foregroundColor(.red)
            
            Text(getFormattedWeekday() + "曜日")
            .font(.system(size: 30))
            .foregroundColor(.yellow)
            
            Text(getFormattedTime())
            .font(.system(size: 30))
            .foregroundColor(.blue)

        }
        .padding()
    }
    
    func getFormattedDate() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd"
        return dateFormatter.string(from: date)
    }
    
    func getFormattedYear() -> String {
        let calendar = Calendar(identifier: .japanese)
        let year = calendar.component(.year, from: date)
        return "令和" + String(year) + "年"
    }

    
    func getFormattedWeekday() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E"
        dateFormatter.locale = Locale(identifier: "ja_JP")
        return dateFormatter.string(from: date)
    }
    
    func getFormattedTime() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "HH:mm:ss"
        return dateFormatter.string(from: date)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(date: Date())
    }
}