2024年4月14日 星期日

檢查 unclodsed(unpaired) html tag 並將其修正 - 使用 Java

在這篇文言,
我想紀錄一下並分享一個用 Java 檢查 HTML unclosed (或 unpaired) tag,並嘗試將其補上缺少的 開頭 tag(openTag)/結尾 tag (closeTag) 的方法。

Java 並不是重點,只要了解邏輯,用什麼程式語言都可以,只是我習慣 Java 所以用 Java 來做例子。

在 HTML 中,除了一些不用成對的 tag ,例如 <img/>, <b/> 等,
大多數的 tag 都是需要成對的,例如 <div> , <p>, <span> 等,需要配合使用 </div>, </p>, </span> 來使之頭尾成對才是合法的 HTML tag。

我們希望能用程式來幫助我們成出一串 HTML 中不成對的非法 tag,
並嘗試將缺少的 openTag 或 closeTag 補上將其修正成合法的 HTML,
要注意到的是,沒有一定正確的修正方法,因為我們不會知道原來寫這串 HTML 的原作者是哪裡忘記少加 openTag/closeTag 了,
例如現在有一串 HTML:

<div>
  xxx
<div>
  xxx

原作者到底原本是想寫

<div>
  xxx
</div>
<div>
  xxx
</div>

還是

<div>
  xxx
  <div>
    xxx
  </div>
</div>

呢?

我們不得而知,而兩種修正方法最後修完的 HTML 也確實都是有成對 tag 的合法 HTML。
在這篇文章裡,我的修法邏輯是採用上述第二種,將缺少的 tag 從外圍補上的方法。

先來直接看最後成品的 Java 程式碼例子,說明及最後的輸出結果都注釋在程式碼中了:

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class HtmlUnclosedTagFix {

	public static void main(String[] args) {
		String htmlStr = "<div><div id='kk'>xxx</p></p><div><div>\n" +
						 "<div><p></p><div>";
		//在這裡我們只修正 div 和 p 這兩個 tag 做例子
		//如果還要修正其他 tag 的話,可以補在 tagToFix 變數中,用 | 符號來分隔
		String tagsToFix = "div|p";
		String regularExpression = "<(\\/?(?:" + tagsToFix + "))(?:\\s+?(?:.*?))?>";
		System.out.println("===========================");
		System.out.println("測試測試的句子:\n" + htmlStr);
		System.out.println("正規表示法: " + regularExpression);

		Pattern pattern = Pattern.compile(regularExpression, Pattern.MULTILINE);
		Matcher matcher = pattern.matcher(htmlStr);

		Deque<String> foundOpenTagStack = new ArrayDeque<>();
		for (int matchCount = 1; matcher.find(); matchCount++) {
			// groupCount不包括匹配的字串,即matcher.group(0)
			System.out.println("---------------------------");
			System.out.println("第" + matchCount + "次匹配,找到" + matcher.groupCount() + "個Group:");
			System.out.println("匹配的字串為: " + matcher.group(0));
			System.out.println("start: " + matcher.start() + ", end: " + matcher.end());

			for (int groupCount = 1; groupCount <= matcher.groupCount(); groupCount++) {
				System.out.println("第" + groupCount + "個Group: " + matcher.group(groupCount));
				String tag = matcher.group(groupCount);

				boolean isOpenTag = !tag.contains("/");
				System.out.println("isOpenTag: " + isOpenTag);
				if (isOpenTag) {
					System.out.println("是 openTag, 將其放進堆疊中");					
					foundOpenTagStack.push(tag);
					showStackContent(foundOpenTagStack);
				} else {
					while (true) {
						if (foundOpenTagStack.isEmpty()) {
							// add missing open tag
							System.out.println("最後還是找不到與 closeTag(" + tag + ") 相對的 openTag,\n"
										     + "代表 closeTag 缺少對應的 openTag,需補上對應的 openTag 做修正");
							htmlStr = "<" + tag.replace("/", "") + ">" + htmlStr;
							System.out.println("修正後的 HTML: " + htmlStr);
							break;
						}

						String openTag = foundOpenTagStack.pop();
						System.out.println("取出堆疊中的 openTag(" + openTag + ")");
						showStackContent(foundOpenTagStack);
						if (openTag.equalsIgnoreCase(tag.replace("/", ""))) {
							// found matched open tag
							System.out.println("找到與 closeTag(" + tag + ")相對應的 openTag,此tag不需修正");
							System.out.println("目前 HTML: " + htmlStr);
							break;
						}

						// add missing end tag
						StringBuilder sb = new StringBuilder(htmlStr);
						sb.insert(matcher.start(), "</" + openTag + ">");
						htmlStr = sb.toString();
						System.out.println("找到與 closeTag(" + tag + ")不相對應的 openTag(" + openTag +"),\n"
									     + "代表 openTag 缺少對應的 closeTag,需補上對應的 closeTag 做修正");
						System.out.println("修正後的 HTML: " + htmlStr);
					}
				}
			}
		}

		// handle left unclosed open tags, add missing close tag
		while (!foundOpenTagStack.isEmpty()) {
			System.out.println("發現堆疊中留有缺少 closeTag 的 openTag 未修正");
			String openTag = foundOpenTagStack.pop();
			System.out.println("取出堆疊中的 openTag");
			showStackContent(foundOpenTagStack);
			htmlStr = htmlStr + "</" + openTag + ">";
			System.out.println("補上 openTag 缺少的相對應 closeTag");
			System.out.println("修正後的 HTML: " + htmlStr);
		}

		System.out.println("===========================");
		System.out.println();
		System.out.println("最後修正完後的 HTML: " + htmlStr);
		
		//輸出的結果:
		/**
		    ===========================
			測試測試的句子:
			<div><div id='kk'>xxx</p></p><div><div>
			<div><p></p><div>
			正規表示法: <(\/?(?:div|p))(?:\s+?(?:.*?))?>
			---------------------------
			第1次匹配,找到1個Group:
			匹配的字串為: <div>
			start: 0, end: 5
			第1個Group: div
			isOpenTag: true
			是 openTag, 將其放進堆疊中
			目前堆疊成員:
			<==>|div|
			---------------------------
			第2次匹配,找到1個Group:
			匹配的字串為: <div id='kk'>
			start: 5, end: 18
			第1個Group: div
			isOpenTag: true
			是 openTag, 將其放進堆疊中
			目前堆疊成員:
			<==>|div|div|
			---------------------------
			第3次匹配,找到1個Group:
			匹配的字串為: </p>
			start: 21, end: 25
			第1個Group: /p
			isOpenTag: false
			取出堆疊中的 openTag(div)
			目前堆疊成員:
			<==>|div|
			找到與 closeTag(/p)不相對應的 openTag(div),
			代表 openTag 缺少對應的 closeTag,需補上對應的 closeTag 做修正
			修正後的 HTML: <div><div id='kk'>xxx</div></p></p><div><div>
			<div><p></p><div>
			取出堆疊中的 openTag(div)
			目前堆疊成員:
			<==>|
			找到與 closeTag(/p)不相對應的 openTag(div),
			代表 openTag 缺少對應的 closeTag,需補上對應的 closeTag 做修正
			修正後的 HTML: <div><div id='kk'>xxx</div></div></p></p><div><div>
			<div><p></p><div>
			最後還是找不到與 closeTag(/p) 相對的 openTag,
			代表 closeTag 缺少對應的 openTag,需補上對應的 openTag 做修正
			修正後的 HTML: <p><div><div id='kk'>xxx</div></div></p></p><div><div>
			<div><p></p><div>
			---------------------------
			第4次匹配,找到1個Group:
			匹配的字串為: </p>
			start: 25, end: 29
			第1個Group: /p
			isOpenTag: false
			最後還是找不到與 closeTag(/p) 相對的 openTag,
			代表 closeTag 缺少對應的 openTag,需補上對應的 openTag 做修正
			修正後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
			<div><p></p><div>
			---------------------------
			第5次匹配,找到1個Group:
			匹配的字串為: <div>
			start: 29, end: 34
			第1個Group: div
			isOpenTag: true
			是 openTag, 將其放進堆疊中
			目前堆疊成員:
			<==>|div|
			---------------------------
			第6次匹配,找到1個Group:
			匹配的字串為: <div>
			start: 34, end: 39
			第1個Group: div
			isOpenTag: true
			是 openTag, 將其放進堆疊中
			目前堆疊成員:
			<==>|div|div|
			---------------------------
			第7次匹配,找到1個Group:
			匹配的字串為: <div>
			start: 40, end: 45
			第1個Group: div
			isOpenTag: true
			是 openTag, 將其放進堆疊中
			目前堆疊成員:
			<==>|div|div|div|
			---------------------------
			第8次匹配,找到1個Group:
			匹配的字串為: <p>
			start: 45, end: 48
			第1個Group: p
			isOpenTag: true
			是 openTag, 將其放進堆疊中
			目前堆疊成員:
			<==>|p|div|div|div|
			---------------------------
			第9次匹配,找到1個Group:
			匹配的字串為: </p>
			start: 48, end: 52
			第1個Group: /p
			isOpenTag: false
			取出堆疊中的 openTag(p)
			目前堆疊成員:
			<==>|div|div|div|
			找到與 closeTag(/p)相對應的 openTag,此tag不需修正
			目前 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
			<div><p></p><div>
			---------------------------
			第10次匹配,找到1個Group:
			匹配的字串為: <div>
			start: 52, end: 57
			第1個Group: div
			isOpenTag: true
			是 openTag, 將其放進堆疊中
			目前堆疊成員:
			<==>|div|div|div|div|
			發現堆疊中留有缺少 closeTag 的 openTag 未修正
			取出堆疊中的 openTag
			目前堆疊成員:
			<==>|div|div|div|
			補上 openTag 缺少的相對應 closeTag
			修正後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
			<div><p></p><div></div>
			發現堆疊中留有缺少 closeTag 的 openTag 未修正
			取出堆疊中的 openTag
			目前堆疊成員:
			<==>|div|div|
			補上 openTag 缺少的相對應 closeTag
			修正後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
			<div><p></p><div></div></div>
			發現堆疊中留有缺少 closeTag 的 openTag 未修正
			取出堆疊中的 openTag
			目前堆疊成員:
			<==>|div|
			補上 openTag 缺少的相對應 closeTag
			修正後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
			<div><p></p><div></div></div></div>
			發現堆疊中留有缺少 closeTag 的 openTag 未修正
			取出堆疊中的 openTag
			目前堆疊成員:
			<==>|
			補上 openTag 缺少的相對應 closeTag
			修正後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
			<div><p></p><div></div></div></div></div>
			===========================
			
			最後修正完後的 HTML: <p><p><div><div id='kk'>xxx</div></div></p></p><div><div>
			<div><p></p><div></div></div></div></div>
		 **/
	}
	
	public static <T> void showStackContent(Deque<T> stack) {
		System.out.println("目前堆疊成員:");
		System.out.print("<==>|");
		stack.forEach((T member) -> {
			System.out.print(member + "|");
		});
		System.out.println("");
	}
}

說明:
在這次的例子中,先以只修正 div 和 p 為例,有需要修正其他的 tag 的話可自行補充到 tagsToFix 變數中 (用 | 符號分隔)。
實現的邏輯很簡單,首先我們要找出所有要修正的 tag 的 openTag 和 closeTag,
尋找 tag 使用了如下的正規表達示:
<(\/?(?:div|p))(?:\s+?(?:.*?))?>

找到了以後,
1. 我們依序看找到的 tag,如果每個 tag 都看過了,就進到第2步,否則進到第1步。

1-1. 如果是屬於 openTag 就放到一個名為 foundOpenTagStack 的堆疊 (Stack) 中,否則到第1-2步。

1-2. 如果是屬於 closeTag,就從堆疊中取出 openTag 來作比較。

1-2-1. 如果被取出的 openTag 和 closeTag 是成對的,那即是代表這兩個 tag 是一對的,不用對 HTML 做修正,並且因為這個 closeTag 已經處理完成,我們可以繼續處理下一個找到的 closeTag,回到第1步。

1-2-2. 如果被取出的 openTag 和 closeTag 是不成對的,那即是代表這個 openTag 缺少了對應的 closeTag,我們便必需對 HTML 做修正,在 HTML 中找到 closeTag 的位置前面補上 openTag 缺少的結尾 tag ,
因為 closeTag 還沒被處理好,可能堆疊中還有與其成對的 openTag,所以回到 1-2 步繼續取出堆疊中的 openTag 做檢查。

1-2-3. 如果堆疊中的 openTag 都拿出來處理過並看過了 (此時堆疊中應該是已經沒有任何東西了),
但還是找不到與 closeTag 相應的 openTag 的話,代表 closeTag 缺少相對應的開頭 Tag,
所以我們必須要修正 HTML,在 HTML 的開頭加上對應的開頭 tag,處理完 closeTag 後,回到第1步繼續處理下一個 closeTag。

2. 當每個 tag 都看過了,此時 closeTag 都處理完畢了,但堆疊中可能還留有未處理的 openTag,例如如果 HTML 是 <div><div> 的話,因為沒有 closeTag,所以上述步驟執行完後會發現堆疊中還留有未處理的 openTag,而此時堆疊中的 openTag 都是缺少成對結尾 tag 的,
所以我們可以一個個把 openTag 取出,並對其修正 HTML,在 HTML 的尾端補上缺少的結尾 tag 。

當上述步驟都做完了以後,我們就能得到被修正好的 HTML 了。

另外補充一點,在 1-2-2 補上 openTag 的 closeTag 時,我們可以利用 matcher.start() 來得知要在 HTML 句子中的哪個位置補上 closeTag,雖然補上 closeTag 後會改變 HTML 句子的長度,但因為下一個要被修正的 openTag 是在更句頭的位置,所以我們不需要擔心被改變長度的 HTML 會影響我們要補上 closeTag 的位置。

而當第一個 closeTag 處理完畢後,堆疊中的所有 openTag 都會被處理完並被取出堆疊的關係,
所以其他的 closeTag 在 1-2-3 補上對應的開頭 tag 時,只要把開頭 tag 加在 HTML 開頭就行了。

在第2步,只有一開始 HTML 裡都沒有 closeTag 時才會發生堆疊中留有 openTag,此時對應的缺少結尾 tag 只要加在 HTML 尾端就行了。

2024年4月13日 星期六

自製表單欄位的浮動標題/標籤 (Floating Field Label/Title)

今天有個需求是想要製作一個
"表單欄位的浮動標題/標籤 (Floating Field Label/Title)",
用自己的想法自製完了以後在這篇文中做個紀錄和分享一下。

Floating Field Label/Title 具體是什麼可以參考 Bootstrap 的一個實現:Floating labels
就是一個被設計放在表單欄位 (例如 <input type="text"/>) 中的 Placeholder,
但當要在欄位上輸入內容時,這個 Floating Field Label 會自行地往上移動讓出欄位的空間給
使用者輸入內容。

需求如下:

  1. Floating Label 平常顯示位置在欄位之中。
  2. 欄位必須要能容納 Floating Label 的所有文字,也就是 Floating Label 內容不能被截斷或超出欄位。
  3. 當欄位被處於 focus 狀態 (例如滑鼠點到欄位使其處於擁有焦點狀態) 或
    欄位處於有被輸入了文字 的情況, Floating Label 必須要往上移動讓出欄位空間,
    同時欄位上方必須要能自動根據 Floating Label 的文字多少預留足夠 Floating Label 上移的空間。

首先先直接看最後的成品,再來對其說明:

HTML:

<div>
  <div class="hidden-text">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ultricies urna nulla, sed viverra purus elementum ut. In hac habitasse platea dictumst. Integer ut dui ligula. Morbi in velit eu urna finibus elementum id consectetur neque.
  </div>
  <div class="wrapper">
    <div>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ultricies urna nulla, sed viverra purus elementum ut. In hac habitasse platea dictumst. Integer ut dui ligula. Morbi in velit eu urna finibus elementum id consectetur neque.
    </div>
    <input type="text" placeholder="" />
    <label>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ultricies urna nulla, sed viverra purus elementum ut. In hac habitasse platea dictumst. Integer ut dui ligula. Morbi in velit eu urna finibus elementum id consectetur neque.
    </label>
  </div>
</div>

CSS:

:root {
  --wrapper-width: 300px;
}

.hidden-text {
  visibility: hidden;
  width: var(--wrapper-width);
}

.wrapper {
  position: relative;
  width: var(--wrapper-width);
}

label {
  position: absolute;
  top: 0px;
  pointer-events: none;
  transition: all 0.25s ease;
}

input:not(:placeholder-shown) + label,
input:focus + label{
  top: -100%;
  transition: all 0.25s ease;
}

input {
    width: var(--wrapper-width);
    height: 100%;
    position: absolute;
    top: 0px;
}

input::placeholder {
  opacity: 0;
}

說明:
HTML 中是我們主要的設計結構,下面這一串文字是用來展示的 Floating Label 測示文字,只是一個 Lorem ipsum

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ultricies urna nulla, sed viverra purus elementum ut. In hac habitasse platea dictumst. Integer ut dui ligula. Morbi in velit eu urna finibus elementum id consectetur neque.

注意到在 HTML 上我們的三個 Lorem ipsum 內容是完全一樣的。
然後為了讓各元件的 width 寬度一致,我們在 css 設定了一個 --wrapper-width 變數給各元件共用:

:root {
  --wrapper-width: 300px;
}

想法是這樣的,我們需要一個能夠依照文字內容自行撐高的 <input type="text"/>,
所以我們將它跟一個文字內容放在同一個 wrapper 裡,這裡我們再放了一個 Floating Label 進去,像這樣:

<div class="wrapper">
    <div>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ultricies urna nulla, sed viverra purus elementum ut. In hac habitasse platea dictumst. Integer ut dui ligula. Morbi in velit eu urna finibus elementum id consectetur neque.
    </div>
    <input class="line2" type="text" placeholder=" " />
    <label>
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ultricies urna nulla, sed viverra purus elementum ut. In hac habitasse platea dictumst. Integer ut dui ligula. Morbi in velit eu urna finibus elementum id consectetur neque.
    </label>
  </div>

我們利用 <div> 來撐高外層的 .wrapper,然後將 <input> 的高設定成跟 .wrapper 一樣,同時設定  positoin:  absolute; top: 0px; 給 <input> 來讓它可以不影響 .wrapper 的高度。
不要忘了設定 position: relative; 給 .warrper,這樣 .wrapper 才能做為 <input> position:absolute; top 位置的基準。

注意到 <inpup> 設定了一個空字串的 placeholder,這是必須的,之後會說明,其是為了能夠被 :placeholder-shown  CSS 選擇器選到。

有一點可以注意到,因為 <input> 在 HTML 是寫在 <div> 之後,所以 <div> 會被 <input> 遮注而不會被看到,這樣我們就不須要特別設定 css 讓 <div> 看不到。

.wrapper {
  position: relative;
  width: var(--wrapper-width);
}

input {
    width: var(--wrapper-width);
    height: 100%;
    position: absolute;
    top: 0px;
}

在看 Floating Label 的 css 之前,我們先看一下最上方的 .hidden-text :

<div class="hidden-text">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus ultricies urna nulla, sed viverra purus elementum ut. In hac habitasse platea dictumst. Integer ut dui ligula. Morbi in velit eu urna finibus elementum id consectetur neque.
  </div>

<div> 的作用是讓 .wrapper 上方有一個預留空間可以讓 Floating Label 移上去,所以用文字將其撐高了以後,我們要讓它在保留空間佔位的情況下將字隱藏起來,在這邊用 visibility: hidden 來達到:

.hidden-text {
  visibility: hidden;
  width: var(--wrapper-width);
}

可以注意到,因為上方的 <div> 和下方 .wrapper 用的文字內容一樣,寬度也設定一樣,所以兩者的高度是一樣的。

接下來就是要來設定 Floating Label,首先,Floating Label 跟 <input> 一樣,必須要不撐高 .wrapper 的空間,所以要設定 position: absolute; top: 0px; 。

再來一個比較特別的地方是,因為 Floating Label 的 HTML 寫在 <input> 之後,會擋到 <input> 讓我們無法點擊到 <input>,所以必須要設定 pointer-events: none; 讓點擊能夠穿特過去。

並且我們給它設定一個動畫效果讓它之後在移位時看起來比較滑順,動畫效果不是必須的,不設定也沒關係 :

label {
  position: absolute;
  top: 0px;
  pointer-events: none;
  transition: all 0.25s ease;
}

最後要來設定移位的 css,首先我們希望 <input> 在被 focus 時,Floating Label 能夠移位上去,這可以用 input:focus + label 選擇器來設定,
但如果 <input> 沒有被 focus ,<input> 中有被輸入文字時,我們不希望 Floating Label 被移位回 <input> 裡,這時之前在 <input> 中設定的 placeholder 就派上用場了,
因為 <input> 設定了 placeholder,在 <input> 中有文字被輸入時,placeholder 就會消失看不見,
此時就可以用 :not(:placeholder-shown) 來選擇 placceholder 看不到時的情況。

所以 CSS 就會像是這樣:

input:not(:placeholder-shown) + label,
input:focus + label {
  top: -100%;
  transition: all 0.25s ease;
}

2024年4月2日 星期二

SQL Server 查詢對上(或下)一行的計算(例如相加或相減等)

這篇要來記錄一下用 SQL Server 來查詢出
對上(或下)一行的計算(例如相加或相減等),


例如:
我們現在有一個 Table ,有 id 和 amount 兩個 INT 的欄位,
放了三筆資料,如下所示:

id amount
3 45
2 25
1 10

我們希望在 id 由大到小排序的情況,得出 amount 一行與上一行的差值,也就是結果應該要像這樣:

increase
id current amount previous amount increase
3 45 25 20
2 25 10 15
1 10 0 10

上面的表列出了某一行的 amount (current amount) 、上一行的 amount (previous amount) 還有
之前的差值 (increase)。

下面直接上 SQL 語法:

-- 先建立測試用 Table
DECLARE @tempTable TABLE (
	id int not null,
	amount int not null,
	PRIMARY KEY (id)
)
-- 塞入測試資料
INSERT INTO @tempTable(id, amount) VALUES(1, 10)
INSERT INTO @tempTable(id, amount) VALUES(2, 25)
INSERT INTO @tempTable(id, amount) VALUES(3, 45)
-- 檢查一下
SELECT *
FROM @tempTable
ORDER BY id DESC;
-- 進行差值計算,
-- 利用 ROW_NUMBER() 來為每行標上序號值 (rowNumber),
-- 得到的 Table 自己跟自己用 JOIN,JOIN 規則是 Table 的某行 (假設序號是 i) 和上一行 (序號是 i - 1) 做 JOIN
WITH romNumberTable AS (
	SELECT *, ROW_NUMBER() OVER (ORDER BY id DESC) AS rowNumber
	FROM (
		SELECT id, amount
		FROM @tempTable
	) romNumberTable
)
SELECT T1.id, T1.amount AS 'current amount', ISNULL(T2.amount, 0) AS 'previous amount', T1.amount - ISNULL(T2.amount, 0) AS 'increase'
FROM romNumberTable T1 LEFT JOIN romNumberTable T2 ON T1.id = T2.id + 1
ORDER BY id DESC

說明:
我們利用 ROW_NUMBER 將排序好的 Table 標上序號,得到了像這樣的 Table:

id amount rowNumber
3 45 1
2 25 2
1 10 3

然後 Table 自己跟自己做 JOIN,JOIN 規則就是 rowNumber 和 rowNumber - 1 做 JOIN,
這樣就可以將 Table 的某行跟上一行 JOIN 再一起了,JOIN 後的結果會像這樣:

id - current amount - current rowNumber - current id - previous amount - previous rowNumber - previous
3 45 1 2 25 2
2 25 2 1 10 3
1 10 3


可以注意到最後一行因為沒有上一行所以 JOIN 不到東西,所以我們使用 LEFT JOIN 來保留沒有東西 JOIN 的那行資料。

成功 JOIN 後,就可以自由地做我們想要做的計算了。

參考資料:

  1. SQL如何计算当前行减去上一行的值

2024年2月29日 星期四

AngularJS - 自製分頁 Filter

紀錄一下 AngularJS 自製分頁 Filter 的方法

HTML:

<div ng-app="app" ng-controller="controller as ctrl">
  <div ng-repeat="item in ctrl.itmeList | paging:ctrl.page:ctrl.pageSize">
    {{item}}
  </div>
  
  <div>  
    Page: <input type="number" ng-model="ctrl.page"/>
  </div>
  <div>  
    Page size: <input type="number" ng-model="ctrl.pageSize"/> Page
  </div>
</div>

Javascript:

var app = angular.module("app", []);
app.controller("controller", [function() {
	var self = this;
  self.itmeList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  self.page = 1;
  self.pageSize = 3;
}]);

app.filter('paging', function() {
	    return function(input, page, pageSize) {
	        return input.slice(page * pageSize - pageSize, page * pageSize);
	    }
});

JsFiddle 範例:

參考資料:

  1. Filters

Javascript 濾掉 Object 中特定屬性的方法

紀錄一下 Javascript 濾掉 Object 中特定屬性的方法,
可以利用 reduce() 來達成,實作程式碼範例如下:

var obj = {"1": 1, "2": 2, "3": 3};
obj = Object.keys(obj).filter(function(key) {
	return key == 1 || key == 3; //濾出值為 1 和 3 的 key
}).reduce(function(newObj, key){
  newObj[key] = obj[key];
	return newObj;
}, {});
console.log(obj); // {1: 1, 3: 3}

Code Point, Surrogate Pair 和 HTML Unicode 編碼

在這篇文章中,我要來紀錄一下 Code Point (字符代碼、碼位、編碼位置) 、Surrogate Pair (代理對)、HTML Unicode 編碼之間的關係,並且示範一下在 Java 中儲存的 UTF-16 字元要如何轉成可在 HTML 中正確顯示的 Unicode 編碼。

首先,先說明一下以下名詞的意義:

  1. Code Point (字符代碼、碼位、編碼位置):
    Code Point 是一個字符的 Unicode 編碼代碼。
    參考 Unicode字元平面對映,Unicode 目前分成 17 組 平面 (Plane),每平面有 65536 (2 的 16 次方),編碼範圍從 0x0000 到 0x10FFFF ,每一個碼位代表某一個字符 (目前還沒有有全部使用到),例如
    "我" 的 Code Point 是 25105 (也就是 0x6211)
    "👍" 的 Code Point 是 128077 (也就是 0x1F44D)
  2. Surrogate Pair (代理對):
    Surrogate Pair 是一種儲存表示字符的一種方式,專門用來儲存 UTF-16 無法用 16 Bit 表示的字符,
    以 "👍" 為例,
    對於 Code Point 是 0xFFFF 以下的字符 UTF-16 會用 16 Bit (2 Byte) 去儲存,
    而對於 "👍" 這種 Code Point 大於 0xFFFF 的字符則會使用 Surrogate Pair 的方式用 32 Bit (4 Byte) 來儲存。

    規則如下,括弧中是以 "👍" 做的例子:
    先將字符的 Code Point 減去 0x10000 (0x1F44D - 0x10000= 0xF44D),
    把得到的值高位部份用 0 填充得到一個 20 Bit 的結果 (0000 1111 0100 0100 1101),
    再將前 10 Bit 高位加上 110110 成為 High Surrogate (1101 1000 0011 1101 = 0xD83D = 55357),
    將後 10 Bit 高位加上 110111 成為 Low Surrogate (1101 1100 0100 1101 = 0xDC4D = 56397),
    最後 UTF-16 會用 High Surrogate + Low Surrogate 為一組的方式 (稱為 Surrogate Pair) 用 32 Bit (4 Byte) 來表示這樣一個字符 ,以  "👍"  來說就是 0xD83D 0xDC4D。

    可以注意到,因為 1101 10 和 1101 11 已作為辨識 High/Low Surrogate 的用途,
    所以 1101 1000 0000 0000 (0xD800) ~ 1101 1111 1111 1111 (0xDFFF) 之間的值不會對應到 任何一個 UTF-16 字符,
    而在 Unicode 中,0xD800 ~ 0xDCFF 之中的 Code Point 也是設計給 UTF-16 的 Surrogate 使用的區塊,
    也就沒有字符的 Code Point 在 0xD800 ~ 0xDCFF 之中。
    可以參考 Unicode字元平面對映

    以 Java 來說,字符是用 UTF-16 來儲存的,
    所以 "👍" 跟 "\ud83d\udc4d" 是等價的。

  3. HTML Unicode 編碼 (或稱 HTML Character Entity 、HTML 字符實體編碼):
    在 HTML 中可以使用 Unicode 表示特殊字符,可以直接使用 10 進位及 16 進位以 Code Point 表示,以 "👍" 為例,可以用 &#128077; 或 &#x1f44d 來表示,
    注意這裡不需使用 Surrogate Pair ,因為 HTML Unicode 是支援大於 0xFFFF 的 Code Point 字符的。

    同樣的,使用 CSS 的時候,也不需要使用 Surrogate Pair,例如:
    <style>
        div:after {
          content: "\1f44d";
        }
      </style>
    

    不過要注意的是,如果是使用 Javascript 的話,還是需要使用 Surrogate Pair 的表達方式,例如一樣以 "👍" 為例:
    console.log("\ud83d\udc4d");
    

最後,用 Java 示範一下上述的觀念,可以配合著看來更好的理解實際上的例子:
使用 JDK 11,
Maven Dependency :

<!-- https://mvnrepository.com/artifact/commons-lang/commons-lang -->
	<dependency>
	    <groupId>commons-lang</groupId>
	    <artifactId>commons-lang</artifactId>
	    <version>2.4</version>
	</dependency>
	
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
	<dependency>
	    <groupId>org.apache.commons</groupId>
	    <artifactId>commons-lang3</artifactId>
	    <version>3.14.0</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text -->
	<dependency>
	    <groupId>org.apache.commons</groupId>
	    <artifactId>commons-text</artifactId>
	    <version>1.11.0</version>
	</dependency>

Java:

package test;

import java.util.HashMap;
import java.util.Map;

public class UTF16Test {

	public static void main(String[] args) {
		//👍
		//Code Point (字符代碼、碼位、編碼位置): 0x1f44d = 128077 (10進位)
		//UTF-16 Surrogate Pair: \ud83d \udc4d = 55357 56397 (10進位)
		//High Surrogate: \ud83d
		//Low Surrogate: \udc4d
		//HTML Character Entity (HTML 字符實體編碼): &128077
		String str = "👍";
		
		//確認是 Code Point = 128077 是否就是 "👍"
		System.out.println(Character.toString(128077).equals(str)); // true
		
		//比較 "👍" 是否與 "\ud83d\udc4d" 等價
		System.out.println("\ud83d\udc4d".equals(str)); // true
		
		//"👍" 字串長度為2,因為是用2個 16 Bit 表示,一個 16 Bit 被當成1個 Char
		System.out.println(str.length()); // 2
		System.out.println(str.toCharArray().length); // 2
		
		//"👍" 只含有一個 Code Point,所以字串的 Code Point 陣列長度是 1
		System.out.println(str.codePoints().toArray().length); // 1
		
		//org.apache.commons.lang.StringEscapeUtils.escapeHtml(String) 
		//無法正確地將 "👍" 編碼成 HTML Character Entity,應該算是 Bug ?
		//輸出是 &#55357;&#56397;
		//這種編碼是無法在 HTML page 裡正常顯示的
		System.out.println(org.apache.commons.lang.StringEscapeUtils.escapeHtml(str)); // &#55357;&#56397;
		
		//org.apache.commons.text.StringEscapeUtils 不會對 👍 進行編碼處理,
		//所以輸出不變一樣是 "👍" ,
		//不過不編碼一樣是不能在 HTML page 裡正常顯示的
		System.out.println(org.apache.commons.text.StringEscapeUtils.escapeHtml4(str)); //👍
		
		//示範如何正確地將 "👍" 轉成可以在 HTML page 正常顯示的 HTML Character Entity,
		//也就是 &#128077;
		System.out.println(convertSurrogatePairToHtmlCode(str)); //&#128077;
		
		//示範如何從 Code Point 計算 Surrogate Pair (包括 High Surrogate 和 Low Surrogate)
		//以 "👍" 為例, Surrogate Pair 就是 \ud83d \udc4d = 55357 56397 (10進位)
		Map<String, Integer> surrogatePair = calculateSurrogatePairFromCodePoint(str.codePointAt(0));
		int highSurrogate = surrogatePair.get("highSurrogate");
		int lowSurrogate = surrogatePair.get("lowSurrogate");
		System.out.println(highSurrogate); // 55357
		System.out.println(lowSurrogate); // 56397
		System.out.println(Character.isHighSurrogate((char) highSurrogate));
		System.out.println(Character.isLowSurrogate((char) lowSurrogate));
		
		//示範如何從 Surrogate Pair 計算 Code Point
		//以 "👍" 為例, Code Point 就是 0x1f44d = 128077 (10進位)
		int codePoint = calculateCodePointFromSurrogatePair(highSurrogate, lowSurrogate);
		System.out.println(codePoint); // 128077
		System.out.println(Character.isSupplementaryCodePoint(codePoint)); // true		
	}
	
	//將文字轉成可以正常顯示的 HTML 編碼
	public static String convertSurrogatePairToHtmlCode(String str) {
		String htmlCode = "";
		
		StringBuilder htmlCodeStringBuilder = new StringBuilder();
		
		str.codePoints().forEach((int codePoint) -> {
			if (Character.isSupplementaryCodePoint(codePoint)) {
				htmlCodeStringBuilder.append(String.format("&#%d;", codePoint));
			} else {
				htmlCodeStringBuilder.append(org.apache.commons.text.StringEscapeUtils.escapeHtml4(Character.toString(codePoint)));
			}
		});
		
		htmlCode = htmlCodeStringBuilder.toString();
		return htmlCode;
	}
	
	//計算一個 Code Point 的 Surrogate Pair (代理對),
	//Surrogate Pair 含有兩部份,分別是 High Surrogate 和 Low Surrogate
	public static Map<String, Integer> calculateSurrogatePairFromCodePoint(int codePoint) {
		Map<String, Integer> surrogatePair = new HashMap<>();
		
		int s = codePoint - 0x10000;
		int highSurrogate = (s >> 10) + 0xd800;
		int lowSurrogate = (s & 0x3ff) + 0xdc00;
		
		surrogatePair.put("highSurrogate", highSurrogate);
		surrogatePair.put("lowSurrogate", lowSurrogate);
		
		return surrogatePair;
	}
	
	//計算一組 High Surrogate 和 Low Surrogate 代表的 Code Point 
	public static int calculateCodePointFromSurrogatePair(int highSurrogate, int lowSurrogate) {

		return ((highSurrogate - 0xd800) << 10) + (lowSurrogate - 0xdc00) + 0x10000;
	}
}

推薦一個線上有人寫的小工具,作者解釋了 Surrogate Pair 並寫了線上幫忙互轉 Code Point 和 Surrogate Pair 的功能:
Surrogate Pair Calculator etc.

參考資料:

  1. The Surrogate Pair Calculator etc.
  2. Unicode Surrogate Pairs
  3. Emoji character sequence &#55357;&#56391; breaks old XML process
  4. How to find the surrogate pair of a symbol in java

2024年2月15日 星期四

Java EE Spring JDBC tmeplate 執行 PostgreSQL 的 Function

這篇要來紀錄如何使用 Java EE Spring JDBC template 來執行
PostgreSQL 的 Function。

首先我們先建立一個測試用的 Table:

兩個 PostgreSQL Function,如以下 SQL 語法:
CREATE TABLE IF NOT EXISTS public.testTable
(
    id integer NOT NULL,
	title text NOT NULL,
	PRIMARY KEY(id) 
)
並且塞入一些資料:
INSERT INTO public.testTable(id, title) VALUES(1, '111');
INSERT INTO public.testTable(id, title) VALUES(2, '222');
INSERT INTO public.testTable(id, title) VALUES(3, '333');
然後建立三個 Function,以下三個 Function 的作用基本一樣,都是接受傳入的 param_id 變數,然後進行
SELECT * FROM testTable WHERE id = param_id
的查詢,不同的是傳回的 type 不同,呼叫 Functoin 的方式也有所不同,
可以注意到的是,有兩個 Function 的名稱一樣 (都取名為 queryUsingReturnedRefcursor),不過 Function 簽名不同 (傳入參數不同),這是為了演示 PostgreSQL 能夠支持同名 Function 故意取名的,類似 Java 的 Function Overload:
  1. queryUsingReturnedSetof(param_id integer):
    CREATE OR REPLACE FUNCTION queryUsingReturnedSetof(param_id integer) RETURNS SETOF testTable AS $$
      SELECT * FROM testTable WHERE id = param_id;
    $$ LANGUAGE sql;
    
    說明:
    此 Function 傳回值 type 為 SETOF,相當於傳回 Table 的資料行集合,呼叫 Function 的方式為如下 SQL 語法:
    SELECT * FROM queryUsingReturnedSetof(2);
  2. queryUsingReturnedRefcursor(param_id integer):
    CREATE OR REPLACE FUNCTION queryUsingReturnedRefcursor(param_id integer) RETURNS refcursor AS $$
    DECLARE ref refcursor;
    BEGIN
      OPEN ref FOR SELECT * FROM testTable WHERE id = param_id;
      RETURN ref; 
    END;
    $$ LANGUAGE plpgsql;
    
    說明:
    此 Function 傳回值 type 為 refcursor,為一個 cursor ,不是 Table 的資料行集合,直接呼叫的話只會得到一個 cursor ,必須要配合 FETCH 指令才能遍歷每一行的資料,例如以下語法:
    SELECT queryUsingReturnedRefcursor(2); FETCH ALL IN "<unnamed portal 6>";
    其中,<unnamed portal 6> 是被回傳的 cursor 名稱,不過在實際上每次執行 Function 後回傳的 cursor name 是會變化的,每次都不一樣,
    而執行完 Function 後, cursor 就消失了,導致執行 FETCH 時會有
    ERROR: cursor "<unnamed portal xxx>" does not exist 
    的錯誤,所以上面那樣的語法沒辦法正確地得到查詢結果資料行,
    要解決上面的問題,可以看下一個 Functoin 的說明,
    其想法是把想要的 cursor name 傳進 Function 以指定回傳的 cursor name。

    不過如果是用 Java 去呼叫的話,是可以成功執行並得到資料行的,
    之後可以看到我們可以用 Java 去指定 cursor name。
  3. queryUsingReturnedRefcursor(ref refcursor, param_id integer):
    CREATE OR REPLACE FUNCTION queryUsingReturnedRefcursor(ref refcursor, param_id integer) RETURNS refcursor AS $$
    BEGIN
      OPEN ref FOR SELECT * FROM testTable WHERE id = param_id;
      RETURN ref; 
    END;
    $$ LANGUAGE plpgsql;
    
    說明:
    此 Function 傳回值 type 為 refcursor,跟上一個 Fucntion 一樣,只差在多傳進一個指定的 cursor name,這樣我們就可以用已知的 cursor name 來取得資料行,呼叫方式如下:
    SELECT queryUsingReturnedRefcursor('xxx_cur', 2); FETCH ALL IN "xxx_cur";
    其中 xxx_cur 可以隨意更換成想要的 cursor name。
接下來是 Java EE Spring 的部份了。
下圖為測試專案的檔案結構,紅框處的是主要專案檔案:






因為重要的地方主要是 DAO 使用 JdbcTemplate 的部份,
為了簡單起見,所以這裡只展示 DAO 的部份,也就是 com.dao.MyDAO.java,
其他 Java EE Spring、Data Source 、使用到的 lib (使用 Maven POM 檔設定) 等的設定可以參考文章最後的源碼下載分享

MyDAO.java :
package com.dao;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcCall;
import org.springframework.stereotype.Repository;

@Repository
public class MyDAO {

	@Autowired
	@Qualifier("postgresql_JdbcTemplate")
	private JdbcTemplate postgresql_JdbcTemplate;
	
	public List<Map> queryUsingReturnedSetof(int id) {
		List<Map> dataList = postgresql_JdbcTemplate.query("SELECT * FROM queryUsingReturnedSetof(?);", new RowMapper<Map>() {		
	        @Override
	        public Map mapRow(ResultSet rs, int rowNumber) throws SQLException {
	        	Map<String, Object> map = new HashMap<>();
	        	
	        	map.put("id", rs.getInt("id"));
	        	map.put("title", rs.getString("title"));
	        	
	            return map;
	        }
	    }, id);
		
		//for test
		for (Map data : dataList) {
			System.out.println(data.get("id") + ", " + data.get("title"));
		}
		
		return dataList;
	}
	
	public List<Map> queryUsingReturnedRefcursor(int id) {
		String customCursorName = "xxx_cur";
		
		SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(postgresql_JdbcTemplate)
				                        .withProcedureName("queryUsingReturnedRefcursor")
				                        .declareParameters(new SqlParameter("id", Types.INTEGER))
				                        .withoutProcedureColumnMetaDataAccess()
				                        .returningResultSet(customCursorName, new RowMapper<Map>() {
				                	        @Override
				                	        public Map mapRow(ResultSet rs, int rowNumber) throws SQLException {
				                	        	Map<String, Object> map = new HashMap<>();
				                	        	
				                	        	map.put("id", rs.getInt("id"));
				                	        	map.put("title", rs.getInt("title"));
				                	        	
				                	            return map;
				                	        }
				                	    });
		
		Map returnedMapContainsCursorName = simpleJdbcCall.execute(new MapSqlParameterSource().addValue("id", id));
		List<Map> dataList = (List) returnedMapContainsCursorName.get(customCursorName);
		
		//for test
		for (Map data : dataList) {
			System.out.println(data.get("id") + ", " + data.get("title"));
		}
		
		return dataList;
	}
}

說明:
queryUsingReturnedSetof(int id) 對應到 PostgreSQL 中的 queryUsingReturnedSetof(param_id integer) 這個 Function。

queryUsingReturnedRefcursor(int id) 對應到 PostgreSQL 中的 queryUsingReturnedRefcursor(param_id integer) 這個 Function,可以看到它可以讓我們指定一個回傳的 refCursor name,
simpleJdbcCall.execute() 並不直接回傳 data list 資料,而是一個 Map,
就像上面我們直接用 Sql 語法查詢那樣回傳的 refCursor 的資料,
我們需要再用指定的 refCursor name 當作 Key 從 Map 中才能獲取真正的 data list 資料。

源碼下載分享: