【Kotlin】微軟的 文字轉換語音(Text To Speach, TTS)

參考資料 ----

文字轉換語音

免費試用語音服務

開始使用文字轉換語音

適用於語音服務的 Azure 訂用帳戶金鑰

Github 範例

cognitive-services-speech-sdk

 

大家應該都聽過 Google 小姐說話,老人家好奇微軟是否也有類似的服務 -- 果然也有 MS 小姐,而且不只小姐,還有先生喔!而且,還不只一位喔。😄

 

用 手機 或 電腦 的瀏覽器,到上面 "參考資料" 的網址,往下捲動,會看到楆題 "透過這個示範應用程式 (採用 JavaScript SDK 建置) 試用文字轉換語音功能",下方的 "文字"  欄可以清除裡面預設的文字,輸入您想要 MS 小姐朗讀的內容。

 

右方 "語言" 預設是 Chinese(Taiwanese Mandarin),還可以切換到 Chinese(Mandarin, Simplified) 簡体中文Chinese(Cantonese, Traditional) 廣東話

 

"語音" 則有 HsiaoChen(Neutral) - 曉臻HsiaoYu(Neutral) - 曉雨YunJhe(Neutral) - 雲哲HanHan - 涵涵Yating - 雅婷Zhiwei - 志威,有 (Neutral) 的表示是以 AI 合成的,標榜聲音更自然,也因此收費較高(2021.08.12 定價表);不過不用担心,不論選擇哪一種語音,每個月有免費額度,超過才需要付費。

 

"說話風格",Taiwanese 語言沒得選說話風格,只有 General,目前只支援简体中文有多種風格。

 

"速度" 應該不需解釋

"音調" 若調低就會像男生,但不像 Google 小姐會變鴨嗓


調整好您的喜好後,按 "播放" 鍵,會有一小段時間的寂靜,猜測應該是系統正在合成語音中,在播放中變更設定是沒有作用的,必須先按 "停止" 再變更。但之後寫成 APP,並不會有網頁的延遲現象。

中英文混著唸大致上也還可以,英文不會怪腔怪調的。

變更 "語言" 或 玩了一陣子後,如果覺得怪怪的,就按一下瀏覽器的 "重新整理"。

 

我去複製了維基的龜兔賽跑,發現 "睡覺" 简体中文會唸 ㄕㄨㄟˋ  ㄐㄩㄝˊ,問了大陸的朋友後,確認這是简体中文的 bug

後來又想,會不會是正體字的關係?於是,我將 "睡覺" 轉成 簡體字 "睡觉",果然發音就正確了 ;我再讓繁體中文的語音去唸簡體字的"睡觉" ,不意外地,也是唸 ㄕㄨㄟˋ  ㄐㄩㄝˊ -- 原來 AI 並沒有把繁簡差異納入考量,以此類推,雖都是英語系國家,同個字可能有不一樣的發音,所以在為該國家(地區) 挑選語音角色時,還是儘量以微軟為該國(地區) 設計的角色為優先選擇。

 

======================================================================


因為要收費,所以您必須具有微軟 Azure 雲端服務帳戶,沒有的話,就去申請一個 -- 參考 免費試用語音服務


登入 Azure 首頁,按一下左上角的 "功能表"

 

點擊 "建立資源"



在 搜尋欄 輸入 speech



輸入相應的資料,有點詭異的是...老人家是第一次使用,但輸入 "名稱" 時卻一直回應說已存在,換了好幾個名字才通過。

"資源群組" 因為是第一次建,所以要按一下 "新建"



出現 "您的部署已完成",按一下 "前往資源"



點擊 "按一下這裡以管理金鑰"



按一下 "顯示金鑰",金鑰會以明碼顯示


有兩組金鑰,只要選 1 組即可

兩組的用意是:

當您想變更金鑰 1 時,新的金鑰並不會立即生效(可能要過 24 小時後),而您的 app、網站、... 提供的服務又不能中斷,這時就可以先改用金鑰 2



系統需求:

Android Studio 3.3 以上

Android 6.0(Marshmallow, API23) 以上


建立新專案

 

修改專案層級的 build.gradle

 

...
...

allprojects {
    repositories {
        google()
        mavenCentral()
        
        // 加這個
        maven {
            url 'https://csspeechstorage.blob.core.windows.net/maven/'
        }
    }
}
 


 


修改 app 層級的 build.gradle

 
...
...

android {
    
    defaultConfig {
        ...
        ...
        
        minSdkVersion 23    // 注意要 23 以上
        
        ...
        ...
    }
    
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    
    ...
    ...
}

dependencies {

    ...
    ...
    
    // Speech SDK
    implementation 'com.microsoft.cognitiveservices.speech:client-sdk:1.18.0'
}
 


AndroidManifest.xml
 
...
...

<uses-permission android:name="android.permission.INTERNET" />

<application
    ...
    ...

 


strings.xml
 
...

<string name="tw">有一位國王,他擁有一座美麗的花園,花園裡有一棵結著金蘋果的樹。國王派人每天清點樹上的蘋果有幾顆。</string>

...
 


activity_main.xml(這是簡化的 layout,重點只在表達放了這些元件)
 
...

<Button
    android:id="@+id/btnSpeak"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onbtnSpeakClicked"
    android:text="speak" />

<Button
    android:id="@+id/btmStop"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="onbtnStopClicked"
    android:text="stop" />

<EditText
    android:id="@+id/edtSpeech"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/tw" />

<TextView
    android:id="@+id/outputMessage"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="outputMessage" />

...
 


MainActivity.kt
 
...

class MainActivity : AppCompatActivity() 
{
    private val TAG = "MainActivity"
    private var speechConfig: SpeechConfig? = null
    private var synthesizer: SpeechSynthesizer? = null
    private lateinit var connection: Connection
    private var audioTrack: AudioTrack? = null
    lateinit var outputMessage: TextView
    private var speakingRunnable: SpeakingRunnable? = null
    private var singleThreadExecutor: ExecutorService? = null
    private val synchronizedObj = Any()
    private var stopped = false
    
    
    override fun onCreate(savedInstanceState: Bundle?)
    {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 請求取得網路授權
        val requestCode = 5 // Unique code for the permission request
        ActivityCompat.requestPermissions(this,
                                            arrayOf(Manifest.permission.INTERNET),
                                            requestCode
                                        )

        singleThreadExecutor = Executors.newSingleThreadExecutor()
        speakingRunnable = SpeakingRunnable()
        outputMessage = findViewById(R.id.outputMessage)
        outputMessage.setMovementMethod(ScrollingMovementMethod())
        audioTrack = AudioTrack(AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            .build(),
            AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(24000) // 音調
                .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                .build(),
            AudioTrack.getMinBufferSize(24000,
                AudioFormat.CHANNEL_OUT_MONO,
                AudioFormat.ENCODING_PCM_16BIT
            ) * 2,
            AudioTrack.MODE_STREAM,
            AudioManager.AUDIO_SESSION_ID_GENERATE
        )
        // 設定說話速度, 正常速度是 1
        val params = PlaybackParams()
        params.speed = 0.7.toFloat()
        audioTrack!!.playbackParams = params

        // 設定音量, float: 0 ~ 1.0
        // audioTrack.setVolume((float) 0.01);
    }
    
    
    override fun onResume()
    {
        super.onResume()
        
        // 初始化
        CreateSynthesizer()
        
        // 連線 Azure 主機
        ConnectAzure()
    }
        
    override fun onDestroy()
    {
        super.onDestroy()

        if (synthesizer != null)
        {
            synthesizer.close()
            connection.close()
        }
        if (speechConfig != null)
        {
            speechConfig.close()
        }
        if (audioTrack != null)
        {
            singleThreadExecutor!!.shutdownNow()
            audioTrack!!.flush()
            audioTrack!!.stop()
            audioTrack!!.release()
        }
    }
    
    
    // 初始化
    fun CreateSynthesizer()
    {
        if (synthesizer != null)
        {
            speechConfig!!.close()
            synthesizer!!.close()
            connection.close()
        }

        Log.d(TAG, "初始化...")
        speechConfig = SpeechConfig.fromSubscription(speechSubscriptionKey, serviceRegion)
        // 使用較好的 24kHz 音質
        speechConfig!!.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Raw24Khz16BitMonoPcm)

        // 設定語音角色
        // 語音列表: https://aka.ms/csspeech/voicenames
        // AI, 合成的聲音更接近真人, 收費較高
        // speechConfig.setSpeechSynthesisVoiceName("en-US-GuyNeural");
        // speechConfig.setSpeechSynthesisVoiceName("en-US-JennyNeural");
        // speechConfig.setSpeechSynthesisVoiceName("en-US-AriaNeural");
        // speechConfig.setSpeechSynthesisVoiceName("zh-TW-HsiaoChenNeural");  // 曉臻

        // 標準
        // speechConfig.setSpeechSynthesisVoiceName("en-US-ZiraRUS");   // 美, Zira
        // speechConfig.setSpeechSynthesisVoiceName("en-GB-HazelRUS");   // 英, Hazel
        // speechConfig.setSpeechSynthesisVoiceName("ja-JP-HarukaRUS");   // 曰, 春香, Haruka
        speechConfig!!.setSpeechSynthesisVoiceName("zh-TW-Yating") // 台灣, 雅婷, 也適用於大陸
        // speechConfig.setSpeechSynthesisVoiceName("zh-HK-Danny");   // 廣東話, Danny
        synthesizer = SpeechSynthesizer(speechConfig, null)
        connection = Connection.fromSpeechSynthesizer(synthesizer)
        val current = resources.configuration.locale
        connection.connected.addEventListener { o: Any?, e: ConnectionEventArgs? -> updateOutputMessage("Connection established.\n")    }
        connection.disconnected.addEventListener { o: Any?, e: ConnectionEventArgs? -> updateOutputMessage("Disconnected.\n")   }
        synthesizer!!.SynthesisStarted.addEventListener { o: Any?, e: SpeechSynthesisEventArgs -> updateOutputMessage(String.format(current,"Synthesis started. Result Id: %s.\n", e.result.resultId))
                                                          e.close()
                                                        }
        synthesizer!!.Synthesizing.addEventListener { o: Any?,
                                                    e: SpeechSynthesisEventArgs -> updateOutputMessage(String.format(current, "Synthesizing. received %d bytes.\n", e.result.audioLength))
                                                      e.close()
                                                    }
        synthesizer!!.SynthesisCompleted.addEventListener { o: Any?, e: SpeechSynthesisEventArgs ->
            updateOutputMessage("Synthesis finished.\n")
            updateOutputMessage("""	First byte latency: ${e.result.properties.getProperty(PropertyId.SpeechServiceResponse_SynthesisFirstByteLatencyMs)} ms.""" )
            updateOutputMessage("""	Finish latency: ${e.result.properties.getProperty(PropertyId.SpeechServiceResponse_SynthesisFinishLatencyMs)} ms.""" )
            e.close()
        }
        synthesizer!!.SynthesisCanceled.addEventListener { o: Any?, e: SpeechSynthesisEventArgs ->
            val cancellationDetails = SpeechSynthesisCancellationDetails.fromResult(e.result).toString()
            updateOutputMessage(""" Error synthesizing. Result ID: ${e.result.resultId}. Error detail: ${System.lineSeparator()}$cancellationDetails${System.lineSeparator()}Did you update the subscription info? """.trimIndent(), true, true)
            e.close()
        }
        synthesizer!!.WordBoundary.addEventListener { o: Any?, e: SpeechSynthesisWordBoundaryEventArgs ->
            updateOutputMessage(String.format(current, 
                                              "Word boundary. Text offset %d, length %d; audio offset %d ms.\n",
                                              e.textOffset,
                                              e.wordLength,
                                              e.audioOffset / 10000
                                            )
                               )
        }
    }
    
    
    // 連線 Azure 主機
    fun ConnectAzure()
    {
        if (connection == null)
        {
            updateOutputMessage("Please initialize the speech synthesizer first\n", true, true)
            return
        }
        connection.openConnection(true)
        updateOutputMessage("Opening connection.\n")
    }
    
    
    // 開始播放
    fun onbtnSpeakClicked(vv: View?)
    {
        clearOutputMessage()
        
        if (synthesizer == null)
        {
            Log.d(TAG, "speech synthesizer not initialized")
            return
        }
        val speakText = findViewById<EditText>(R.id.edtSpeech)
        speakingRunnable!!.setContent(speakText.text.toString())
        singleThreadExecutor!!.execute(speakingRunnable)
    }
    
    
    // 停止播放
    fun onbtnStopClicked(vv: View?)
    {
        if (synthesizer == null)
        {
            Log.d(TAG, "speech synthesizer not initialized")
            return
        }
        stopSynthesizing()
    }
    
    
    private fun stopSynthesizing()
    {
        if (synthesizer != null)
        {
            synthesizer!!.StopSpeakingAsync()
        }
        if (audioTrack != null)
        {
            synchronized(synchronizedObj) { stopped = true }
            audioTrack!!.pause()
            audioTrack!!.flush()
        }
    }
    
    
    internal inner class SpeakingRunnable : Runnable
    {
        private var content: String? = null
        fun setContent(content: String?)
        {
            this.content = content
        }

        override fun run()
        {
            try
            {
                audioTrack!!.play()
                synchronized(synchronizedObj)
                {
                    stopped = false
                }
                val result = synthesizer!!.StartSpeakingTextAsync(content).get()
                val audioDataStream = AudioDataStream.fromResult(result)

                // Set the chunk size to 50 ms. 24000 * 16 * 0.05 / 8 = 2400
                val buffer = ByteArray(2400)
                while (!stopped)
                {
                    val len = audioDataStream.readData(buffer)
                    if (len == 0L)
                    {
                        break
                    }
                    audioTrack!!.write(buffer, 0, len.toInt())
                }
                audioDataStream.close()
            }
            catch (ex: Exception)
            {
                Log.e("Speech Synthesis Demo", "unexpected " + ex.message)
                ex.printStackTrace()
                assert(false)
            }
        }
    }
    
    
    private fun updateOutputMessage(text: String)
    {
        updateOutputMessage(text, false, true)
    }

    
    @Synchronized
    private fun updateOutputMessage(text: String, error: Boolean, append: Boolean)
    {
        runOnUiThread 
        {
            if (append)
                {
                    outputMessage.append(text)
                }
            else
                {
                    outputMessage.text = text
                }
            if (error)
            {
                val spannableText = outputMessage.text as Spannable
                spannableText.setSpan(ForegroundColorSpan(Color.RED),
                                      spannableText.length - text.length,
                                      spannableText.length,
                                      0
                                     )
            }
        }
    }
    
    
    private fun clearOutputMessage()
    {
        updateOutputMessage("", false, false)
    }
    

    companion object
    {
        // 申請的金鑰
        private const val speechSubscriptionKey = "blablablablablablablablabla"
        // 申請的 Azure 主機所在區域
        private const val serviceRegion = "eastasia"
    }

}
 


* 微軟支援多種語音輸出格式,如 rawoggmp3...,您可參考 SpeechSynthesisOutputFormat,以 音訊、網路連線 品質做為評估要點,選擇您喜好的格式;不過本筆記的程式碼只能播 Raw...Pcm,要播其他格式,應該需要調整播放方式。

在程式碼播放格式為 Raw24Khz16BitMonoPcm,若改為 Raw16Khz16BitMonoPcm 則音調變高且速度變快


* 如果您有玩過黑膠唱片,就知道將唱機轉速變慢,聲音就會變低沈;而 AudioTrackAPI22(含) 之前就是類似黑膠的效果, 但 API23 將速度放慢,聲調並不會變低沈,可能這也是微軟要求 minSdkVersion 23 的理由吧。

 

 * 似乎按 STOP 鈕後再按 SPEAK 鈕,就是從頭開始,沒辦法從停止處再接下去播放。 

目前想到的解法是:

因為是唸一段文章,中間必定會有標點符號,那就將一整段文章做分割(split) ,存入 List<String> 內,再逐個元表取出朗讀;但也不能切太小段,因為似乎也有提出請求的數量的限制,目前踹出來的可行方式是以句點做為切割點,分段落切。

 

* 語音長度有限制,請參考 Text-to-Speech Quotas and limits per Speech resource ,若是長篇文章,要想辦法切成 10 分鐘內的片段。



然後,老人家又好奇了,微軟有 AI 語音,那 Google 有嗎?還真的有!而且 Amazon 也有,不過後兩者都沒有微軟的豐富,另外老人家覺得微軟的語音夠用了,就懶的再試了😁,有興趣的看倌就自行摸索吧。


相關筆記 ----

【Kotlin】TextToSpeech -- 請 Google 小姐朗讀

 

 

【Oracle】簡易計算年齡的方法

參考資料 ----

EXTRACT (datetime)

TO_DATE

 
    -- 以 2018 年為例
    -- 先以 TO_DATE 將字串欄位轉成日期型態
    -- 再以 EXTRACE 取出 其 出生年度
    -- 然後與 2018 相減, 就可得出 年齡了
    SELECT 2018-EXTRACT(YEAR  FROM  TO_DATE(birthday,'yyyymmdd')) age  FROM  table1
 


【Linux】設定 Rocky Linux 為 GUI 圖形介面模式

下載最小安裝 DVD iso


選擇 Server最小安裝


安裝完成後,會是文字介面,以 root 登入



[root]# dnf group list

會列出可安裝的群組類型

[root]# dnf groupinstall "Server with GUI" -y

再設定以 GUI 模式開機

[root]# systemctl set-default graphical

重新開機,就 ok

[root]# reboot

【Kotlin】SQLite 以 BIG5 排序

參考資料 ----

BIG5 碼線上查詢系統


這是個山不轉路轉的作法 😝


繁體中文習慣以 BIG5 排序,但 SQLiteORDER BY 是依 UTF8 排序,既然如此,那我們就轉個彎,在資料表內新增一個欄位,存放 BIG5 內碼,當需要排序時,就 ORDER BY 這個 BIG5 內碼欄位。

 

例如:

我們有個資料表,存放考試科目,則建立 2 個欄位 exname(考試科目名稱)、exbig5,當新增一筆記錄時,就查出 exname 的中文 BIG5 內碼 並存入 exbig5;當需要排序時,就 ORDER BY exbig5


先產生中文字的 BIG5 內碼表(參考底下的相關筆記)


將內碼表置於 /專案目錄/asset/


MAP 讀入內碼表(這屬於耗時的工作,應交由 Thread、AsyncTask 或 Coroutine 執行)


新增儲存記錄時,查詢 exname 內(逐字)的 BIG5 內碼,所得到的字串存入 exbig5


之後的查詢,若需要排序 exname 時,就改排序 exbig5



相關筆記 ----

【C#】以程式列出 中文字 與 BIG5 內碼 的對應表



【MySQL】簡體中文的 編碼與排序關係

參考資料 ----

MySQL Chinese, Japanese, and Korean Character Sets

一图弄懂ASCII、GB2312、GBK、GB18030编码

 

探索了正體中文的排序,好奇...那簡體中文呢?

 

MySQL 對簡體中文在 UTF8 時的排序似乎看不出規律性

一层楼, 一枕奇, 七侠五义, 万花楼, 三侠五义, 三国志, 三国演义, 三字经, 世说新语, 东周列国志, 九章算术, 乾隆下江南, 二刻拍案惊奇...

中間突然冒出了筆劃很多的 "乾隆下江南"


因為大陸是漢語拼音,所以若以 gb18030 編碼排序,看似是以拼音字母順序排列

-- 按漢語拼音排序
SELECT * FROM 資料表 ORDER BY CONVERT(欄位名 USING gb18030) 

結果如下:

八段锦, 八美图, 白圭志, 白虎通, 百家姓, 北里志, 北梦琐言, ...

"" 開頭的書名被排到後面去了!整個排序跟上面完全不同!


如果仿照正體中文的排序法呢?

SELECT * FROM 資料表 ORDER BY CONVERT(SUBSTR(欄位名,1,1) USING gb18030), BINARY 欄位名

結果如下:

八段锦, 八美图, 白圭志, 白虎通, 百家姓, 北梦琐言, 北里志, ...

沒有很大差異,僅 北梦琐言, 北里志 前後對調

 

相關筆記 ----

【MySQL】查詢結果按正體中文排序



【CentOS7】 以 smbclient 指令連線 Windows 共享主機

參考資料 ----

SAMBA smbclient



Linux 系統的電腦要連線 Windows server 主機,必須要安裝 SAMBA 套件,然後就可以在圖形操作界面環境連線 Windows 主機,進行檔案相關操作。

 

如果要以指令方式,則是 smbclient


瀏覽

[user]$ smb  -L //IP/目錄名  -U 使用者帳號

要注意:通常網管不會直接開放 Windows 的根目錄出來共享,所以 Windows 主機的 IP 後應該會接目錄名稱。

接著會出現要您輸入密碼,若輸錯密碼,就會回到

[user]$ 

重新下 smbclient 指令

當密碼正確,就會列出 Windows 主機有開放共享的目錄及檔案



登入操作

[user]$ smb  //IP -U 使用者帳號

登入成功後,會看見提示字元

smb:\>

接下來的操作,類似 FTP 的指令模式,不過指令較陽春,例如:

 

 

列出目前 Windows 目錄下的檔案及子目錄

dirls



切換目錄

cd 切換目錄,還可以輸入中文

smb:\>cd 目錄名



下載檔案

get 完整檔案名稱(含副檔名)

會下載到您 Linux 當時所在的目錄,例如:

在您輸入 smbclient 要連線 Windows 主機時,所在的 Linux 目錄為 /tmp ,則 get 下來的檔案就會存放在 /tmp

get 完整檔案名稱(含副檔名)  /指定目錄/另存檔名

當要下載到指定目錄時,就必須輸入檔名,也就是說下面的指令是行不通

get 完整檔案名稱(含副檔名)  /指定目錄/



上傳檔案

put 本地檔案名

沒有來源目錄,就是目前的 Linux 本機目錄

沒有指定目的地目錄,就是上傳到目前的 Windows 目錄


離開

exit



【CentOS7】Visual Studio Code 無法啟動

老人家安裝在 CentOS 7Visual Studio Code 已經有一陣子無法運作了 -- 只要一啟動 vscode,就僵在啟動畫面,目前的版本為 1.57.0

 

暫時先下載安裝 Sublime 頂著用。

 

可是...我還是想用 vscode 啊...


爬文後,參考網友的做法,先到 Microsoft YUM repos 找到舊的 1.52.1 版,手動下載安裝,並停用自動更新。

 
* 移除目前的 vscode 1.57.0 版
[root]# yum remove code

* 切換到下載下來的 1.52.1 的目錄(我是下載到 /tmp)
[root]# yum install code-1.52.1-1608137084.el7.x86_64.rpm

* 停止 vscode 自動更新
[root]# vi /etc/yum.repos.d/vscode.repo
* 啟動了 vi, 按 i 進入編輯模式
* 將 enabled=1  改為 enabled=0
* 按 :x 存檔離開
 

重開機,就可以使用 vscode

 

如果重開機後,Gnome 的圖形界面功能表--"軟體開發" 內仍沒有 vscode 的圖示,則可以

開啟終端機視窗,輸入

/usr/share/code/code

手動啟動 vscode


【PHP】以個體導向(物件導向) 方式進行日期的運算

 

 
// 設定時區
date_default_timezone_set('Asia/Taipei');


// 建立 日期時間個體(物件)
$date = new DateTime();    // 未帶參數時, 就是現在

$date = new DateTime('2000-01-01');    // 帶參數, 就是指定日期
                                       // 沒有時間參數, 就是 00:00:00
                                       // 以下同此例
                                       // 老人家較關心日期,所以就不管時間了!   XD

// 日期往後(日期時間的加法)
echo '方法一:加 10 月';
$date->add(new DateInterval('P10M'));	// P 表 period, M 表 "月"
echo $date->format('Y-m-d') . "\n";	// 格式化顯示 西元 年-月-日, 相當於 date() 函式


echo '方法二:加 2 天';
$date->modify('+2 day');
echo $date->format('Y-m-d') . "\n";


// 日期往前(日期時間的減法)
echo '方法一:減 8 月 10 天';
$date->sub(new DateInterval('P8M10D'));
echo $date->format('Y-m-d') . "\n";

echo '方法二:減 2 天';
$date->modify('-2 day');
echo $date->format('Y-m-d') . "\n";


echo '當月 1 日';
$date->modify('first day of');
echo $date->format('Y-m-d') . "\n";


echo '當月月底';
$date->modify('last day of');
echo $date->format('Y-m-d') . "\n";


echo '前一天';
$date->modify('yesterday');
echo $date->format('Y-m-d') . "\n";


echo '後一天';
$date->modify('tomorrow');
echo $date->format('Y-m-d') . "\n";


echo '當月第一個週一';
$date->modify('first mon of');    // 大小寫無差, 完整寫法 monday 也行
echo $date->format('Y-m-d') . "\n";


echo '當月最後一個週六';
$date->modify('last sat of');
echo $date->format('Y-m-d') . "\n";


echo '移至離當日最近的下個週四';
$date->modify('thursday');
echo $date->format('Y-m-d') . "\n";


echo '移至下個週五';
$date->modify('fri');  // 因為隔天就是週五, 所以只移了 1 天
echo $date->format('Y-m-d') . "\n";


echo '移至下個週三';
$date->modify('wed');
echo $date->format('Y-m-d') . "\n";


echo '移至下個月';
$date->modify('next month');    // 似乎無法搭配 day, ex: next day
                                // 也不能搭數字, ex: next 2 month
echo $date->format('Y-m-d') . "\n";

// 前一年的年初(第一天)
$date->modify('first day of january last year');

// 前一年的年底(最後一天)
$date->modify('last day of december last year');


// 日期的複製
// 不能直接宣告一個新的變數, 這樣有點類似 "指標"
$date2 = $date    // 這樣其實是將 $date2 指向 $date
                  // 要將 $date2 看做是 $date 的別名
// 當變更了 $date2 的值, 其實是變更 $date

// 應該要
$date2 = new DateTime($date->format('Y-m-d')); 
 

 

 

關於上述例子中,簡單英文日期時間的參數格式,可以參考 Relative Formats

 

【Delphi】MySQL 3.51 ODBC connection string


 
Driver={MySQL ODBC 3.51 Driver};Server=主機IP;Database=資料庫名稱;User=連線帳號;Password=密碼;Option=3;

// 如果不是走預設的 3306 port, 而是用自定的連線埠
Driver={MySQL ODBC 3.51 Driver};Server=主機IP;Database=資料庫名稱;Port=自定的連線埠;User=連線帳號;Password=密碼;Option=3;

// 自定資料庫使用的字元集
Driver={MySQL ODBC 3.51 Driver};Server=主機IP;charset=UTF8;Database=資料庫名稱;Port=自定的連線埠;User=連線帳號;Password=密碼;Option=3;
 


【Delphi】在 ProgressBar 內顯示進度文字

Form 上放一個 ProgressBar(name=progressBar)Label(name=lblProgress)


 
lblProgress.Parent := progressBar;
lblProgress.AutoSize := False;
lblProgress.Transparent := True;
lblProgress.Top :=  0;
lblProgress.Left :=  0;
lblProgress.Width := progressBar.ClientWidth;
lblProgress.Height := progressBar.ClientHeight;
lblProgress.Alignment := taLeftJustify;
lblProgress.Layout := tlCenter;