โปรเจกต์: โปรแกรมคำนวณ Union & Intersection ของ 2 เซต พร้อมวาด Venn Diagram (MATLAB)
สมาชิก: 1) อณุภา โตมีบุญ (2511310795) · 2) ปภาวิน เต้าทิพย์ภูมิรักษ์ (2511310605)
งาน: งานกลุ่ม CPE-211 · นำเสนอ พุธ 14 ตุลาคม 2569 · ห้อง D502 · เวลารวมประมาณ 14 นาที
เอกสารนี้เป็นสคริปต์สำหรับพูด อธิบายทีละไฟล์ เน้นโค้ดและการทำงาน ใช้คู่กับภาพในโฟลเดอร์ docs/ (flowchart, วิธีคิดมือ, ผลลัพธ์)
| ช่วง | หัวข้อ | เวลา | ผู้นำเสนอ |
|---|---|---|---|
| 1 | เปิด + โจทย์ + ภาพรวม | 2 นาที | คนที่ 1 |
| 2 | แผนผังการทำงาน (flowchart) | 2 นาที | คนที่ 1 |
| 3 | อธิบายทีละไฟล์ (โค้ด) | 8 นาที | คนที่ 1–2 |
| 4 | เดโมสด | 1.5 นาที | ทั้งคู่ |
| 5 | สรุป + Q&A | 0.5 นาที | คนที่ 2 |
| รวม | 14 นาที |
"สวัสดีครับ/ค่ะ พวกเราคือกลุ่ม ... มีสมาชิก 2 คน ได้แก่ อณุภา โตมีบุญ และ ปภาวิน เต้าทิพย์ภูมิรักษ์"
"โจทย์ของเราเป็นเรื่อง การดำเนินการทางเซต (Set Operations) ในวิชา CPE-211 กำหนดให้เซต A และ B แล้วให้หา Union (A ∪ B) และ Intersection (A ∩ B) พร้อมนำเสนอผลลัพธ์เป็น Venn Diagram"
โจทย์ที่ใช้สาธิต
"เราคิดวิธีทำด้วยมือก่อน แล้วจึงเขียนโปรแกรม MATLAB เพื่อให้คำนวณได้กับเซตขนาดใหญ่หรือซ้ำกันได้อัตโนมัติ"
เปิดภาพ docs/result_manual.png เพื่อโชว์วิธีคิดมือ
"ผลที่ได้คือ A ∪ B มี 7 ตัว (101–107) และ A ∩ B มี 3 ตัว (103, 104, 105) ขั้นตอนนี้คิดมือได้ไม่ยาก แต่ถ้าเซตมีเป็นหลักพัน การเขียนโปรแกรมจะตอบได้ทันที"
เปิดภาพ docs/flowchart.png
"ภาพนี้คือ flowchart การทำงานของโปรแกรมครับ/ค่ะ ขั้นตอนคือ"
แผนผังการเรียกใช้ไฟล์ (dependency)
main_script.m
├── read_set.m ──────► parse_set_input.m
├── calculate_set_ops.m
└── plot_venn.m
run_tests.m
├── parse_set_input.m
└── calculate_set_ops.m
"หัวใจสำคัญคือ โปรแกรมถูกแยกเป็นฟังก์ชันย่อย แต่ละไฟล์ทำงานเฉพาะอย่าง ทำให้อ่านง่าย ทดสอบง่าย และนำไปใช้ซ้ำได้"
"ไฟล์นี้เป็นตัวควบคุมหลัก ทำหน้าที่รับค่า → คำนวณ → แสดงผล → วาดรูป"
1) เตรียมและรับค่า
clc; clear; close all;
setA = read_set('Set A');
setB = read_set('Set B');
2) ตัดค่าซ้ำก่อนคำนวณ
nRawA = numel(setA);
setA = unique(setA, 'stable');
if nRawA > numel(setA)
fprintf('หมายเหตุ: Set A มีค่าซ้ำ %d ตัว ระบบตัดออกให้แล้ว\n', nRawA - numel(setA));
end
3) คำนวณใน try/catch
try
[set_U, set_I] = calculate_set_ops(setA, setB);
only_A = setdiff(setA, setB); % อยู่ใน A แต่ไม่อยู่ใน B
only_B = setdiff(setB, setA); % อยู่ใน B แต่ไม่อยู่ใน A
catch err
fprintf(2, 'เกิดข้อผิดพลาดในการคำนวณ: %s\n', err.message);
return;
end
4) ส่งผลให้ฟังก์ชันวาดรูป
res = struct('setA', setA, 'setB', setB, 'union', set_U, ...
'intersect', set_I, 'onlyA', only_A, 'onlyB', only_B);
plot_venn(res, 'NameA', 'Set A', 'NameB', 'Set B');
"หน้าที่คือถามผู้ใช้กรอกเซต และวนถามซ้ำจนกว่าจะได้ค่าที่ถูกต้อง"
while true
raw = input(sprintf('กรุณากรอกสมาชิกของ %s ...: ', label), 's');
v = local_try_variable(raw);
if ~isempty(v)
values = v; return;
end
[v, ok, msg] = parse_set_input(raw);
if ok
values = v; return;
end
fprintf(2, ' [ผิดพลาด] %s กรุณาลองใหม่\n', msg);
end
function v = local_try_variable(raw)
if isempty(s) || ~isvarname(s), return; end
if evalin('base', sprintf('exist(''%s'', ''var'')', s)) ~= 1, return; end
val = evalin('base', s);
if isnumeric(val) && isvector(val)
v = val(:).';
end
end
"ไฟล์นี้ใจดีกับผู้ใช้มาก รับ input ได้หลายแบบ เช่น [1 2 3], 1,2,3, 1:5"
ขั้นที่ 1–2: ทำความสะอาดข้อความ
str = regexprep(str, '[\[\]\(\)\{\}]', ' '); % ตัดวงเล็บ [] () {}
str = regexprep(str, '[;,]+', ' '); % เปลี่ยน , ; ให้เป็นช่องว่าง
tokens = regexp(str, '\S+', 'match'); % แยกเป็นคำๆ
ขั้นที่ 3: แปลงทีละคำ (รองรับ range)
if ~isempty(strfind(t, ':')) % เจอ ':' = เป็นช่วง เช่น 1:5 หรือ 1:2:9
parts = str2double(strsplit(t, ':'));
if numel(parts) < 2 || numel(parts) > 3 || any(isnan(parts))
ok = false; msg = sprintf('รูปแบบช่วง (range) ไม่ถูกต้อง: %s', t); return;
end
switch numel(parts)
case 2, r = parts(1):parts(2);
case 3, r = parts(1):parts(2):parts(3);
end
else
v = str2double(t);
if isnan(v) % แปลงไม่ได้ = ไม่ใช่ตัวเลข
ok = false; msg = sprintf('ค่าที่ไม่ใช่ตัวเลข: %s', t); return;
end
vals = [vals, v];
end
"นี่คือฟังก์ชันคำนวณหลัก มี signature แบบนี้"
function [set_union, set_intersect] = calculate_set_ops(A, B, varargin)
1) ตรวจสอบ input และอ่านตัวเลือก
if ~isnumeric(A) || (~isempty(A) && ~isvector(A))
error('calculate_set_ops:badInput', 'Set A ต้องเป็นเวกเตอร์ตัวเลข');
end
...
switch lower(name)
case 'stable'
stable = logical(val);
case 'tolerance'
if ~isnumeric(val) || ~isscalar(val) || val < 0
error('calculate_set_ops:badOption', 'Tolerance ต้องเป็นจำนวนจริงไม่ติดลบ');
end
tol = double(val);
otherwise
error('calculate_set_ops:badOption', 'ไม่รู้จักตัวเลือก: %s', name);
end
2) คำนวณ
if tol > 0
A = round(A ./ tol) .* tol; % ปัดค่าลงบนกริดของ tolerance ก่อนเทียบ
B = round(B ./ tol) .* tol;
end
if stable
set_union = union(A, B, 'stable');
set_intersect = intersect(A, B, 'stable');
else
set_union = union(A, B);
set_intersect = intersect(A, B);
end
"วาดวงกลม 2 วง ใส่จำนวนสมาชิกในแต่ละโซน และปรับขนาดข้อความอัตโนมัติ"
1) วาดวงกลม 2 วง
theta = linspace(0, 2*pi, 300);
x_A = r * cos(theta) - 0.75; y_A = r * sin(theta);
x_B = r * cos(theta) + 0.75; y_B = r * sin(theta);
fill(x_A, y_A, [0.2 0.6 0.8], 'FaceAlpha', 0.4, 'EdgeColor', [0.1 0.4 0.7]);
fill(x_B, y_B, [0.9 0.5 0.2], 'FaceAlpha', 0.4, 'EdgeColor', [0.8 0.3 0.1]);
axis equal; axis off;
2) ปรับขนาดฟอนต์ตามจำนวนสมาชิก
nMax = max([numel(onlyA), numel(onlyB), numel(set_I)]);
if nMax <= 8
titleFs = 14; labelFs = 11; valueFs = 10;
elseif nMax <= 20
titleFs = 13; labelFs = 10; valueFs = 9;
...
3) เลือกฟอนต์ไทยอัตโนมัติ
candidates = {'TH Sarabun New', 'Sarabun', 'Leelawadee UI', 'Leelawadee', ...
'Tahoma', 'Arial Unicode MS', 'Angsana New', 'Cordia New', ...
'Noto Sans Thai', 'Sukhumvit Set'};
"เราเขียน test ไว้ 10 เคส เพื่อพิสูจน์ว่าโค้ดทำงานถูกจริง"
tests = { @test_parse_formats; @test_parse_ranges; ... };
for k = 1:numel(tests)
try
tests{k}();
fprintf(' PASS %s\n', func2str(tests{k}));
nPass = nPass + 1;
catch err
fprintf(' FAIL %s -> %s\n', func2str(tests{k}), err.message);
nFail = nFail + 1;
end
end
"ขอสาธิตการใช้งานจริงครับ/ค่ะ"
เคล็ดลับ: ซ้อมเดโมล่วงหน้า และเตรียมค่าที่จะพิมพ์ไว้ให้พร้อม
"สรุปคือ โปรแกรมนี้รับเซต 2 เซต แล้วหา Union / Intersection พร้อมวาด Venn Diagram จุดเด่นคือ รับ input ได้หลากหลาย ตรวจสอบข้อมูลและถามซ้ำได้ คำนวณแม่นยำ และมีชุดทดสอบยืนยันผล ขอบคุณครับ/ค่ะ ยินดีตอบคำถาม"
| คำถาม | แนวคำตอบ |
|---|---|
| ทำไมใช้ unique ก่อนคำนวณ? | เพื่อให้จำนวนสมาชิกถูกต้อง เพราะเซตไม่นับค่าซ้ำ และแจ้งผู้ใช้ว่าตัดไปกี่ตัว |
| setdiff ใช้ทำอะไร? | หาสมาชิกที่อยู่เฉพาะเซตหนึ่ง เอาไปแสดงในโซน A-only / B-only ของแผนภาพ |
| 'stable' ต่างจากปกติอย่างไร? | ปกติผลจะเรียงจากน้อยไปมาก แต่ 'stable' คงลำดับที่ป้อนเดิม |
| Tolerance มีไว้ทำไม? | เทียบเลขทศนิยมที่คลาดเคลื่อน เช่น 0.1+0.2 ไม่เท่ากับ 0.3 เป๊ะๆ |
| ทำไมใช้ input(..., 's')? | รับเป็นข้อความล้วน ปลอดภัยกว่า ไม่ให้ผู้ใช้รันโค้ดผ่าน input |
| ถ้าเซตว่างจะพังไหม? | ไม่พัง มีเทสต์ test_edge_empty ยืนยันว่าได้ผลลัพธ์ว่างถูกต้อง |
| try/catch จำเป็นไหม? | จำเป็น เพื่อไม่ให้โปรแกรมหยุดกลางทาง และแจ้ง error ให้ผู้ใช้ทราบ |
| คน | รับผิดชอบพูด | ไฟล์ที่เชี่ยวชาญ |
|---|---|---|
| คนที่ 1 | เปิด + ภาพรวม + main_script.m + calculate_set_ops.m + plot_venn.m | main_script.m, calculate_set_ops.m, plot_venn.m |
| คนที่ 2 | read_set.m + parse_set_input.m + run_tests.m + สรุป + เดโม | read_set.m, parse_set_input.m, run_tests.m |
สมาชิกทุกคนต้องมานำเสนอ และควรช่วยกันตอบ Q&A
โค้ดจริงจากไฟล์ในโปรเจกต์ (คัดลอกได้)
% ==========================================================
% MATLAB Project: Interactive Set Union & Intersection
% Visualisation: Venn Diagram Representation
% เวอร์ชันปรับปรุง (MATLAB_DRIVE_v2)
% - รองรับ input หลากหลายรูปแบบ และตรวจสอบข้อมูล
% - จัดการค่าซ้ำอย่างถูกต้อง
% - แผนภาพเวนน์ปรับขนาดได้ + ฟอนต์ไทย
% - คงผลลัพธ์ของฟังก์ชันเดิมไว้ครบถ้วน
% ==========================================================
clc; clear; close all;
fprintf('====================================================\n');
fprintf(' โปรแกรมคำนวณหา Union และ Intersection ทางเซต\n');
fprintf('====================================================\n');
fprintf('รูปแบบที่ป้อนได้ เช่น [1 2 3], 1,2,3, 1;2;3, 1:5, {1 2 3}\n');
fprintf('หรือพิมพ์ชื่อตัวแปรตัวเลขที่มีอยู่ใน Workspace ก็ได้\n\n');
% ----------------------------------------------------------
% 1. รับค่า Input จาก User (พร้อมตรวจสอบและถามซ้ำถ้าผิด)
% ----------------------------------------------------------
setA = read_set('Set A');
setB = read_set('Set B');
% ตัดค่าซ้ำออก (พร้อมรายงานจำนวนที่ตัด) เพื่อให้จำนวนสมาชิกถูกต้อง
nRawA = numel(setA);
nRawB = numel(setB);
setA = unique(setA, 'stable');
setB = unique(setB, 'stable');
if nRawA > numel(setA)
fprintf('หมายเหตุ: Set A มีค่าซ้ำ %d ตัว ระบบตัดออกให้แล้ว\n', nRawA - numel(setA));
end
if nRawB > numel(setB)
fprintf('หมายเหตุ: Set B มีค่าซ้ำ %d ตัว ระบบตัดออกให้แล้ว\n', nRawB - numel(setB));
end
% ----------------------------------------------------------
% 2. เรียกใช้ Function คำนวณ Set Operations
% ----------------------------------------------------------
try
[set_U, set_I] = calculate_set_ops(setA, setB);
only_A = setdiff(setA, setB); % อยู่ใน A แต่ไม่อยู่ใน B
only_B = setdiff(setB, setA); % อยู่ใน B แต่ไม่อยู่ใน A
catch err
fprintf(2, 'เกิดข้อผิดพลาดในการคำนวณ: %s\n', err.message);
return;
end
% ----------------------------------------------------------
% 3. แสดงผลลัพธ์ใน Command Window
% ----------------------------------------------------------
fprintf('\n----------------------------------------------------\n');
fprintf(' ผลลัพธ์การคำนวณ\n');
fprintf('----------------------------------------------------\n');
fprintf('Set A : %s (จำนวน %d ตัว)\n', num2str(setA), numel(setA));
fprintf('Set B : %s (จำนวน %d ตัว)\n', num2str(setB), numel(setB));
fprintf('1. Union (A U B) : %s (จำนวน %d ตัว)\n', num2str(set_U), numel(set_U));
fprintf('2. Intersection (A n B): %s (จำนวน %d ตัว)\n', num2str(set_I), numel(set_I));
fprintf('3. A เท่านั้น (A - B) : %s (จำนวน %d ตัว)\n', num2str(only_A), numel(only_A));
fprintf('4. B เท่านั้น (B - A) : %s (จำนวน %d ตัว)\n', num2str(only_B), numel(only_B));
fprintf('----------------------------------------------------\n\n');
% ----------------------------------------------------------
% 4. วาดกราฟ Venn Diagram
% ----------------------------------------------------------
res = struct('setA', setA, 'setB', setB, 'union', set_U, ...
'intersect', set_I, 'onlyA', only_A, 'onlyB', only_B);
try
plot_venn(res, 'NameA', 'Set A', 'NameB', 'Set B');
catch err
fprintf(2, 'วาดแผนภาพไม่สำเร็จ: %s\n', err.message);
end
function values = read_set(label)
%READ_SET ถามผู้ใช้กรอกเซตซ้ำจนกว่าจะได้ค่าที่ถูกต้อง
%
% values = read_set(label)
%
% รองรับทั้งการพิมพ์ตัวเลขในรูปแบบต่างๆ (ดู parse_set_input)
% และการพิมพ์ชื่อตัวแปรตัวเลขที่มีอยู่ใน Workspace
while true
raw = input(sprintf('กรุณากรอกสมาชิกของ %s (เช่น [101 102 103 104 105]): ', label), 's');
v = local_try_variable(raw);
if ~isempty(v)
values = v;
return;
end
[v, ok, msg] = parse_set_input(raw);
if ok
values = v;
return;
end
fprintf(2, ' [ผิดพลาด] %s กรุณาลองใหม่\n', msg);
end
end
% -------------------------------------------------------------------------
function v = local_try_variable(raw)
%LOCAL_TRY_VARIABLE ถ้า raw เป็นชื่อตัวแปรตัวเลขใน Workspace ให้คืนค่าเวกเตอร์นั้น
v = [];
s = strtrim(raw);
if isempty(s) || ~isvarname(s)
return;
end
try
if evalin('base', sprintf('exist(''%s'', ''var'')', s)) ~= 1
return;
end
val = evalin('base', s);
catch
return;
end
if isnumeric(val) && isvector(val)
v = val(:).';
end
end
function [values, ok, msg] = parse_set_input(raw)
%PARSE_SET_INPUT แปลงข้อความที่ผู้ใช้กรอกให้เป็นเวกเตอร์ตัวเลขแถวเดียว
%
% [values, ok, msg] = parse_set_input(raw)
%
% รองรับรูปแบบการป้อนที่หลากหลาย เช่น
% [1 2 3] {1,2,3} (1 2 3)
% 1 2 3 1,2,3 1;2;3
% -3.5, 0, 2.25 1:5 1:2:9
% [1 2 3; 4 5 6] (เมทริกซ์ จะถูกคลี่เป็นแถวเดียว)
% 1e3 1000 +7
%
% Input : raw - ข้อความดิบจากผู้ใช้ (string หรือ char)
% Output: values - เวกเตอร์แถวของตัวเลข (ว่างถ้า parse ไม่สำเร็จ)
% ok - true ถ้า parse สำเร็จ
% msg - ข้อความอธิบายสาเหตุเมื่อ ok เป็น false
values = [];
ok = true;
msg = '';
if nargin < 1 || isempty(raw)
ok = false;
msg = 'Input ว่างเปล่า';
return;
end
str = strtrim(char(raw));
if isempty(str)
ok = false;
msg = 'Input ว่างเปล่า';
return;
end
% 1) ตัดวงเล็บ/ปีกกา/วงกลมทั้งหมดออก (รองรับทั้งแบบครบและไม่ครบ)
str = regexprep(str, '[\[\]\(\)\{\}]', ' ');
% 2) เปลี่ยนตัวคั่น comma/semicolon ให้เป็นช่องว่าง
str = regexprep(str, '[;,]+', ' ');
str = strtrim(str);
if isempty(str)
ok = false;
msg = 'ไม่พบตัวเลขใน Input';
return;
end
% 3) แยกเป็น token และแปลงทีละตัว (รองรับ range ด้วย ':')
tokens = regexp(str, '\S+', 'match');
vals = [];
for k = 1:numel(tokens)
t = tokens{k};
if ~isempty(strfind(t, ':')) %#ok<STREMP>
parts = str2double(strsplit(t, ':'));
if numel(parts) < 2 || numel(parts) > 3 || any(isnan(parts))
ok = false;
msg = sprintf('รูปแบบช่วง (range) ไม่ถูกต้อง: %s', t);
return;
end
switch numel(parts)
case 2
r = parts(1):parts(2);
case 3
r = parts(1):parts(2):parts(3);
end
if isempty(r)
ok = false;
msg = sprintf('ช่วงว่างหรือค่าขั้น (step) เป็น 0: %s', t);
return;
end
vals = [vals, r]; %#ok<AGROW>
else
v = str2double(t);
if isnan(v)
ok = false;
msg = sprintf('ค่าที่ไม่ใช่ตัวเลข: %s', t);
return;
end
vals = [vals, v]; %#ok<AGROW>
end
end
if isempty(vals)
ok = false;
msg = 'ไม่พบตัวเลขใน Input';
return;
end
values = vals(:).';
end
function [set_union, set_intersect] = calculate_set_ops(A, B, varargin)
%CALCULATE_SET_OPS คำนวณ Union และ Intersection ของ 2 เซต
%
% [set_union, set_intersect] = calculate_set_ops(A, B)
% [set_union, set_intersect] = calculate_set_ops(A, B, 'Stable', true)
% [set_union, set_intersect] = calculate_set_ops(A, B, 'Tolerance', 1e-9)
%
% Input : A, B - เวกเตอร์ตัวเลข
% Output: set_union - สมาชิกทั้งหมดแบบไม่ซ้ำ (A U B) : เวกเตอร์แถว
% set_intersect - สมาชิกที่ซ้ำกัน (A n B) : เวกเตอร์แถว
%
% ตัวเลือก (name-value, ไม่บังคับ):
% 'Stable' (logical, ค่าเริ่มต้น false)
% true = คงลำดับการป้อนเดิม ('stable')
% false = เรียงลำดับจากน้อยไปมาก (เหมือนฟังก์ชันเดิม)
% 'Tolerance' (จำนวนจริง >= 0, ค่าเริ่มต้น 0)
% 0 = เทียบแบบเป๊ะๆ (เหมือนฟังก์ชันเดิม)
% >0 = ปัดค่าลงบนกริดของ tolerance ก่อนเทียบ
% เหมาะกับเลขทศนิยมที่อาจคลาดเคลื่อน
%
% หมายเหตุ: เมื่อไม่ส่งตัวเลือกใดๆ ผลลัพธ์จะเหมือนฟังก์ชันเวอร์ชันเดิมทุกประการ
[A, B, stable, tol] = local_parseOptions(A, B, varargin{:});
if tol > 0
A = round(A ./ tol) .* tol;
B = round(B ./ tol) .* tol;
end
if stable
set_union = union(A, B, 'stable');
set_intersect = intersect(A, B, 'stable');
else
set_union = union(A, B);
set_intersect = intersect(A, B);
end
end
% -------------------------------------------------------------------------
function [A, B, stable, tol] = local_parseOptions(A, B, varargin)
stable = false;
tol = 0;
if ~isnumeric(A) || (~isempty(A) && ~isvector(A))
error('calculate_set_ops:badInput', 'Set A ต้องเป็นเวกเตอร์ตัวเลข');
end
if ~isnumeric(B) || (~isempty(B) && ~isvector(B))
error('calculate_set_ops:badInput', 'Set B ต้องเป็นเวกเตอร์ตัวเลข');
end
if mod(numel(varargin), 2) ~= 0
error('calculate_set_ops:badOption', 'ตัวเลือกต้องส่งมาเป็นคู่ name-value');
end
for k = 1:2:numel(varargin)
name = varargin{k};
val = varargin{k + 1};
if ~ischar(name)
error('calculate_set_ops:badOption', 'ชื่อตัวเลือกต้องเป็นข้อความ');
end
switch lower(name)
case 'stable'
stable = logical(val);
case 'tolerance'
if ~isnumeric(val) || ~isscalar(val) || val < 0
error('calculate_set_ops:badOption', 'Tolerance ต้องเป็นจำนวนจริงไม่ติดลบ');
end
tol = double(val);
otherwise
error('calculate_set_ops:badOption', 'ไม่รู้จักตัวเลือก: %s', name);
end
end
A = A(:).';
B = B(:).';
end
function hFig = plot_venn(res, varargin)
%PLOT_VENN วาดแผนภาพเวนน์ 2 เซต พร้อมจำนวน/สมาชิกในแต่ละโซน
%
% plot_venn(res)
% plot_venn(res, 'NameA', 'Set A', 'NameB', 'Set B', ...
% 'MaxItems', 12, 'FontName', 'Tahoma', 'Title', '...')
%
% Input : res - struct ที่มีฟิลด์
% .setA, .setB, .union, .intersect, .onlyA, .onlyB
%
% ตัวเลือก (name-value, ไม่บังคับ):
% 'NameA'/'NameB' - ชื่อที่ใช้แสดงของแต่ละเซต
% 'MaxItems' - จำนวนสมาชิกสูงสุดที่แสดงต่อโซน (ค่าเริ่มต้น 12)
% 'FontName' - ฟอนต์ที่ใช้ (ถ้าไม่ระบุ จะเลือกรองรับภาษาไทยอัตโนมัติ)
% 'Title' - หัวเรื่องของแผนภาพ
%
% Output: hFig - handle ของ figure ที่สร้างขึ้น
% -------------------- อ่านตัวเลือก --------------------
nameA = 'Set A';
nameB = 'Set B';
maxItems = 12;
fontName = '';
titleStr = 'แผนภาพเวนน์แสดงผลลัพธ์การดำเนินการทางเซต (Venn Diagram)';
for k = 1:2:numel(varargin)
switch lower(varargin{k})
case 'namea', nameA = varargin{k + 1};
case 'nameb', nameB = varargin{k + 1};
case 'maxitems', maxItems = varargin{k + 1};
case 'fontname', fontName = varargin{k + 1};
case 'title', titleStr = varargin{k + 1};
otherwise
error('plot_venn:badOption', 'ไม่รู้จักตัวเลือก: %s', varargin{k});
end
end
if isempty(fontName)
fontName = local_pickThaiFont();
end
onlyA = res.onlyA;
onlyB = res.onlyB;
set_I = res.intersect;
set_U = res.union;
% -------------------- กำหนดขนาดตามจำนวนสมาชิก --------------------
nMax = max([numel(onlyA), numel(onlyB), numel(set_I)]);
if nMax <= 8
titleFs = 14; labelFs = 11; valueFs = 10;
elseif nMax <= 20
titleFs = 13; labelFs = 10; valueFs = 9;
elseif nMax <= 50
titleFs = 12; labelFs = 9; valueFs = 7;
else
titleFs = 11; labelFs = 8; valueFs = 6;
end
% -------------------- สร้างรูป --------------------
hFig = figure('Name', 'Venn Diagram Visualisation', ...
'NumberTitle', 'off', 'Color', 'w');
% วงกลม 2 วง (ตำแหน่งเดิมเพื่อให้หน้าตาเหมือนเวอร์ชันแรก)
r = 1.5;
theta = linspace(0, 2*pi, 300);
x_A = r * cos(theta) - 0.75; y_A = r * sin(theta);
x_B = r * cos(theta) + 0.75; y_B = r * sin(theta);
fill(x_A, y_A, [0.2 0.6 0.8], 'FaceAlpha', 0.4, ...
'EdgeColor', [0.1 0.4 0.7], 'LineWidth', 2);
hold on;
fill(x_B, y_B, [0.9 0.5 0.2], 'FaceAlpha', 0.4, ...
'EdgeColor', [0.8 0.3 0.1], 'LineWidth', 2);
axis equal;
axis off;
xlim([-3.4 3.4]);
ylim([-3.6 2.4]);
title(titleStr, 'FontSize', titleFs, 'FontWeight', 'bold', 'FontName', fontName);
% -------------------- ข้อความในแต่ละโซน --------------------
strA = local_fmt_list(sort(onlyA), maxItems, 22);
strB = local_fmt_list(sort(onlyB), maxItems, 22);
strI = local_fmt_list(sort(set_I), maxItems, 22);
% Set A Only (ซ้าย)
text(-1.4, 0.5, sprintf('%s Only\n(%d ตัว)', nameA, numel(onlyA)), ...
'HorizontalAlignment', 'center', 'FontWeight', 'bold', ...
'FontSize', labelFs, 'Color', [0.1 0.3 0.6], 'FontName', fontName);
text(-1.4, -0.4, strA, 'HorizontalAlignment', 'center', ...
'FontSize', valueFs, 'Color', [0.2 0.2 0.2], 'FontName', fontName);
% Set B Only (ขวา)
text(1.4, 0.5, sprintf('%s Only\n(%d ตัว)', nameB, numel(onlyB)), ...
'HorizontalAlignment', 'center', 'FontWeight', 'bold', ...
'FontSize', labelFs, 'Color', [0.7 0.2 0.1], 'FontName', fontName);
text(1.4, -0.4, strB, 'HorizontalAlignment', 'center', ...
'FontSize', valueFs, 'Color', [0.2 0.2 0.2], 'FontName', fontName);
% Intersection (กลาง)
text(0, 0.5, sprintf('Intersection (%s n %s)\n(%d ตัว)', nameA, nameB, numel(set_I)), ...
'HorizontalAlignment', 'center', 'FontWeight', 'bold', ...
'FontSize', labelFs, 'Color', [0.2 0.5 0.2], 'FontName', fontName);
text(0, -0.4, strI, 'HorizontalAlignment', 'center', ...
'FontSize', valueFs, 'FontWeight', 'bold', 'Color', 'k', 'FontName', fontName);
% Union (สรุปด้านล่าง)
unionStr = local_wrap_text(sprintf('จำนวนสมาชิกรวมทั้งหมด Union (%s U %s) = %d ตัว', ...
nameA, nameB, numel(set_U)), 60);
unionStr = sprintf('%s\nสมาชิก: %s', unionStr, local_fmt_list(sort(set_U), maxItems, 60));
text(0, -2.6, unionStr, 'HorizontalAlignment', 'center', 'FontWeight', 'bold', ...
'FontSize', valueFs, 'FontName', fontName, ...
'BackgroundColor', [0.95 0.95 0.95], 'EdgeColor', [0.7 0.7 0.7], 'Margin', 8);
hold off;
end
% =========================================================================
function s = local_fmt_list(v, maxItems, wrapWidth)
%LOCAL_FMT_LIST แปลงเวกเตอร์ตัวเลขเป็นข้อความ {a, b, c} แบบจำกัดจำนวน
v = v(:).';
if isempty(v)
s = '{}';
return;
end
total = numel(v);
tail = '';
if total > maxItems
v = v(1:maxItems);
tail = sprintf(', … +%d', total - maxItems);
end
isInt = all(abs(v - round(v)) < 1e-9);
parts = cell(1, numel(v));
for k = 1:numel(v)
if isInt
parts{k} = sprintf('%d', round(v(k)));
else
parts{k} = sprintf('%g', v(k));
end
end
s = ['{', strjoin(parts, ', '), tail, '}'];
s = local_wrap_text(s, wrapWidth);
end
% =========================================================================
function out = local_wrap_text(s, width)
%LOCAL_WRAP_TEXT แทรกขึ้นบรรทัดใหม่เมื่อข้อความยาวเกิน width
if numel(s) <= width
out = s;
return;
end
words = strsplit(s, ' ');
lines = {};
cur = '';
for k = 1:numel(words)
w = words{k};
if isempty(cur)
cur = w;
elseif numel(cur) + 1 + numel(w) <= width
cur = [cur ' ' w]; %#ok<AGROW>
else
lines{end + 1} = cur; %#ok<AGROW>
cur = w;
end
end
if ~isempty(cur)
lines{end + 1} = cur;
end
out = sprintf('%s\n', lines{:});
out = out(1:end - 1);
end
% =========================================================================
function fn = local_pickThaiFont()
%LOCAL_PICKTHAIFONT เลือกฟอนต์ที่รองรับภาษาไทยจากที่มีในระบบ
try
fn = get(0, 'defaultAxesFontName');
catch
fn = 'Helvetica';
end
try
avail = listfonts();
catch
return;
end
candidates = {'TH Sarabun New', 'Sarabun', 'Leelawadee UI', 'Leelawadee', ...
'Tahoma', 'Arial Unicode MS', 'Angsana New', 'Cordia New', ...
'Noto Sans Thai', 'Sukhumvit Set'};
for k = 1:numel(candidates)
if any(strcmpi(avail, candidates{k}))
fn = candidates{k};
return;
end
end
end
function run_tests()
%RUN_TESTS ชุดทดสอบสำหรับฟังก์ชันในโปรเจกต์ (ไม่ต้องพึ่งพา toolbox เพิ่มเติม)
%
% รันจากโฟลเดอร์นี้ด้วยคำสั่ง: run_tests
%
% ครอบคลุม: การ parse input หลากหลายรูปแบบ, พฤติกรรมเริ่มต้นที่ต้องเหมือน
% เวอร์ชันเดิม, ตัวเลือก Stable/Tolerance, และ edge case ต่างๆ
fprintf('=== Running tests ===\n');
tests = {
@test_parse_formats
@test_parse_ranges
@test_parse_invalid
@test_parse_matrix_flatten
@test_default_matches_builtin
@test_stable_order
@test_tolerance_floats
@test_duplicates_dedupe
@test_edge_empty
@test_bad_input_error
};
nPass = 0;
nFail = 0;
for k = 1:numel(tests)
name = func2str(tests{k});
try
tests{k}();
fprintf(' PASS %s\n', name);
nPass = nPass + 1;
catch err
fprintf(' FAIL %s -> %s\n', name, err.message);
nFail = nFail + 1;
end
end
fprintf('=== %d passed, %d failed ===\n', nPass, nFail);
if nFail > 0
error('run_tests:failed', '%d test(s) failed', nFail);
end
end
% =========================================================================
function test_parse_formats
cases = {
'[1 2 3]', [1 2 3]
'1 2 3', [1 2 3]
'1,2,3', [1 2 3]
'1;2;3', [1 2 3]
'{1, 2, 3}', [1 2 3]
'(1 2 3)', [1 2 3]
' -3.5, 0, 2.25 ', [-3.5 0 2.25]
'1e3', 1000
'+7', 7
'[1 2 3', [1 2 3] % วงเล็บไม่ครบ ก็ยังคงผ่อนปรนให้
};
for k = 1:size(cases, 1)
[v, ok, msg] = parse_set_input(cases{k, 1});
assert(ok, 'parse ล้มเหลว: "%s" (%s)', cases{k, 1}, msg);
assert_close(v, cases{k, 2}, 1e-12, cases{k, 1});
end
end
% =========================================================================
function test_parse_ranges
[v1, ok1] = parse_set_input('1:5');
assert(ok1, 'range 1:5 ควร parse ได้');
assert(isequal(v1, [1 2 3 4 5]), 'range 1:5 ผลไม่ถูกต้อง');
[v2, ok2] = parse_set_input('1:2:9');
assert(ok2, 'range 1:2:9 ควร parse ได้');
assert(isequal(v2, [1 3 5 7 9]), 'range 1:2:9 ผลไม่ถูกต้อง');
[v3, ok3] = parse_set_input('-2:2');
assert(ok3 && isequal(v3, [-2 -1 0 1 2]), 'range -2:2 ผลไม่ถูกต้อง');
[v4, ok4] = parse_set_input('[1 2 3] 10:12');
assert(ok4 && isequal(v4, [1 2 3 10 11 12]), 'ผสม list + range ผลไม่ถูกต้อง');
end
% =========================================================================
function test_parse_invalid
bad = {'', 'abc', '1,2,x', '1::3', ':', '1:', '--5', '1.2.3', 'NaN'};
for k = 1:numel(bad)
[~, ok] = parse_set_input(bad{k});
assert(~ok, 'ควร parse ไม่ผ่าน: "%s"', bad{k});
end
end
% =========================================================================
function test_parse_matrix_flatten
[v, ok] = parse_set_input('[1 2 3; 4 5 6]');
assert(ok, 'เมทริกซ์ควร parse ได้');
assert(isequal(v, [1 2 3 4 5 6]), 'การคลี่เมทริกซ์ไม่ถูกต้อง');
end
% =========================================================================
function test_default_matches_builtin
A = [3 1 2 2];
B = [2 4];
[U, I] = calculate_set_ops(A, B);
assert(isequal(U, union(A, B)), 'default Union ต้องเหมือน union() เป๊ะ');
assert(isequal(I, intersect(A, B)), 'default Intersect ต้องเหมือน intersect() เป๊ะ');
end
% =========================================================================
function test_stable_order
A = [3 1];
B = [2 1];
[U, I] = calculate_set_ops(A, B, 'Stable', true);
assert(isequal(U, [3 1 2]), 'Stable Union ควรคงลำดับการป้อน');
assert(isequal(I, [1]), 'Stable Intersect ผลไม่ถูกต้อง');
end
% =========================================================================
function test_tolerance_floats
A = 0.1 + 0.2; % ~0.30000000000000004
B = 0.3;
[~, Iexact] = calculate_set_ops(A, B);
assert(isempty(Iexact), 'แบบเป๊ะๆ ควรได้ intersection ว่าง');
[~, Itol] = calculate_set_ops(A, B, 'Tolerance', 1e-9);
assert(numel(Itol) == 1, 'แบบ tolerance ควรจับคู่เลขทศนิยมได้');
assert(abs(Itol - 0.3) < 1e-12, 'ค่าที่ได้ควรใกล้ 0.3');
end
% =========================================================================
function test_duplicates_dedupe
v = [5 5 1 1 3];
u = unique(v, 'stable');
assert(isequal(u, [5 1 3]), 'unique stable ควรคงลำดับแรกที่พบ');
assert(numel(v) - numel(u) == 2, 'จำนวนค่าซ้ำที่ตัดต้องเป็น 2');
end
% =========================================================================
function test_edge_empty
[U, I] = calculate_set_ops([], []);
assert(isempty(U) && isempty(I), 'เซตว่างทั้งคู่ควรได้ผลว่าง');
[U2, I2] = calculate_set_ops([1 2], []);
assert(isequal(U2, [1 2]) && isempty(I2), 'B ว่าง ผลไม่ถูกต้อง');
end
% =========================================================================
function test_bad_input_error
didThrow = false;
try
calculate_set_ops('abc', [1 2]); %#ok<CTCH>
catch
didThrow = true;
end
assert(didThrow, 'input ที่ไม่ใช่ตัวเลขควร throw error');
end
% =========================================================================
function assert_close(a, b, tol, label)
assert(isequal(size(a), size(b)), 'ขนาดไม่ตรงกัน: %s', label);
assert(max(abs(a(:) - b(:))) < tol, 'ค่าไม่ตรงกัน: %s', label);
end