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如何计算当前行减去上一行的值