using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L19-1: 공학 계산기 핸들러. "calc" 프리픽스로 사용합니다. /// /// 예: calc → 사용법 목록 /// calc sin 45 → sin(45°) = 0.7071 /// calc cos 60 → cos(60°) = 0.5 /// calc tan 45 → tan(45°) = 1.0 /// calc sqrt 144 → √144 = 12 /// calc log 1000 → log₁₀(1000) = 3 /// calc ln 2.718 → ln(2.718) ≈ 1.0 /// calc pow 2 10 → 2¹⁰ = 1024 /// calc factorial 10 → 10! = 3628800 /// calc gcd 12 18 → GCD(12,18) = 6 /// calc lcm 4 6 → LCM(4,6) = 12 /// calc pi → π = 3.14159265358979 /// calc e → e = 2.71828182845905 /// calc deg 1.5707 → 라디안 → 도 변환 /// calc rad 90 → 도 → 라디안 변환 /// calc abs -42 → 절댓값 /// calc ceil 3.2 → 올림 /// calc floor 3.8 → 내림 /// calc round 3.567 2 → 반올림 (소수점 자리) /// Enter → 결과 복사. /// public class CalcHandler : IActionHandler { public string? Prefix => "calc"; public PluginMetadata Metadata => new( "Calc", "공학 계산기 — sin·cos·log·sqrt·factorial·GCD·LCM 등", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("공학 계산기", "calc sin/cos/tan/sqrt/log/ln/pow/factorial/gcd/lcm/pi/e/deg/rad/abs/ceil/floor/round", null, null, Symbol: "\uE8EF")); items.Add(BuildUsage("삼각함수", "calc sin 45 / calc cos 60 / calc tan 30")); items.Add(BuildUsage("제곱근·거듭제곱", "calc sqrt 144 / calc pow 2 10")); items.Add(BuildUsage("로그", "calc log 1000 / calc ln 2.718")); items.Add(BuildUsage("팩토리얼", "calc factorial 12")); items.Add(BuildUsage("GCD · LCM", "calc gcd 12 18 / calc lcm 4 6")); items.Add(BuildUsage("상수", "calc pi / calc e")); items.Add(BuildUsage("단위 변환", "calc deg 1.5707 / calc rad 90")); items.Add(BuildUsage("기타", "calc abs -42 / calc ceil 3.2 / calc floor 3.8 / calc round 3.567 2")); return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); var fn = parts[0].ToLowerInvariant(); // 단순 상수 if (fn is "pi") { var v = Math.PI; return Result(items, "π (Pi)", v, "π"); } if (fn is "e") { var v = Math.E; return Result(items, "e (자연상수)", v, "e"); } if (fn is "phi") { var v = 1.6180339887; return Result(items, "φ (황금비)", v, "φ"); } // 단일 인수 함수 if (parts.Length >= 2 && double.TryParse(parts[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var a)) { switch (fn) { case "sin": return Result(items, $"sin({a}°)", Math.Sin(DegToRad(a)), "sin"); case "cos": return Result(items, $"cos({a}°)", Math.Cos(DegToRad(a)), "cos"); case "tan": if (Math.Abs(a % 180 - 90) < 1e-9) { items.Add(ErrorItem("tan(90°±n·180°)는 정의되지 않습니다 (무한대)")); return Task.FromResult>(items); } return Result(items, $"tan({a}°)", Math.Tan(DegToRad(a)), "tan"); case "asin": if (a < -1 || a > 1) { items.Add(ErrorItem("asin 입력값은 -1 ~ 1 범위여야 합니다")); break; } return Result(items, $"asin({a}) °", RadToDeg(Math.Asin(a)), "asin", isAngle: true); case "acos": if (a < -1 || a > 1) { items.Add(ErrorItem("acos 입력값은 -1 ~ 1 범위여야 합니다")); break; } return Result(items, $"acos({a}) °", RadToDeg(Math.Acos(a)), "acos", isAngle: true); case "atan": return Result(items, $"atan({a}) °", RadToDeg(Math.Atan(a)), "atan", isAngle: true); case "sqrt": if (a < 0) { items.Add(ErrorItem("음수의 실수 제곱근은 정의되지 않습니다")); break; } return Result(items, $"√{a}", Math.Sqrt(a), "sqrt"); case "cbrt": return Result(items, $"∛{a}", Math.Cbrt(a), "cbrt"); case "log": if (a <= 0) { items.Add(ErrorItem("log 입력값은 양수여야 합니다")); break; } return Result(items, $"log₁₀({a})", Math.Log10(a), "log10"); case "log2": if (a <= 0) { items.Add(ErrorItem("log2 입력값은 양수여야 합니다")); break; } return Result(items, $"log₂({a})", Math.Log2(a), "log2"); case "ln": if (a <= 0) { items.Add(ErrorItem("ln 입력값은 양수여야 합니다")); break; } return Result(items, $"ln({a})", Math.Log(a), "ln"); case "exp": return Result(items, $"e^{a}", Math.Exp(a), "exp"); case "factorial": { var n = (long)Math.Round(a); if (n < 0 || n > 20) { items.Add(ErrorItem("팩토리얼은 0~20 범위만 지원합니다")); break; } var fac = Factorial(n); items.Add(new LauncherItem($"{n}! = {fac:N0}", $"팩토리얼 · Enter 복사", null, ("copy", $"{fac}"), Symbol: "\uE8EF")); items.Add(new LauncherItem("결과", $"{fac}", null, ("copy", $"{fac}"), Symbol: "\uE8EF")); return Task.FromResult>(items); } case "abs": return Result(items, $"|{a}|", Math.Abs(a), "abs"); case "ceil": return Result(items, $"⌈{a}⌉", Math.Ceiling(a), "ceil"); case "floor": return Result(items, $"⌊{a}⌋", Math.Floor(a), "floor"); case "round": { int decimals = 0; if (parts.Length >= 3 && int.TryParse(parts[2], out var d)) decimals = Math.Clamp(d, 0, 15); var rounded = Math.Round(a, decimals, MidpointRounding.AwayFromZero); return Result(items, $"round({a}, {decimals})", rounded, "round"); } case "deg": return Result(items, $"{a} rad → °", RadToDeg(a), "deg", isAngle: true); case "rad": return Result(items, $"{a}° → rad", DegToRad(a), "rad"); case "sign": items.Add(new LauncherItem($"sign({a}) = {Math.Sign(a)}", a > 0 ? "양수" : a < 0 ? "음수" : "0", null, ("copy", $"{Math.Sign(a)}"), Symbol: "\uE8EF")); return Task.FromResult>(items); } } // 이인수 함수 if (parts.Length >= 3 && double.TryParse(parts[1], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var x) && double.TryParse(parts[2], System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var y)) { switch (fn) { case "pow": return Result(items, $"{x}^{y}", Math.Pow(x, y), "pow"); case "log": if (x <= 0 || y <= 0 || y == 1) { items.Add(ErrorItem("밑과 진수는 양수, 밑 ≠ 1 이어야 합니다")); break; } return Result(items, $"log_({x}) {y}", Math.Log(y, x), "log"); case "atan2": return Result(items, $"atan2({x},{y}) °", RadToDeg(Math.Atan2(x, y)), "atan2", isAngle: true); case "hypot": return Result(items, $"hypot({x},{y})", Math.Sqrt(x * x + y * y), "hypot"); case "gcd": { var ga = (long)Math.Abs(Math.Round(x)); var gb = (long)Math.Abs(Math.Round(y)); var gv = GcdLong(ga, gb); items.Add(new LauncherItem($"GCD({ga}, {gb}) = {gv}", "최대공약수 · Enter 복사", null, ("copy", $"{gv}"), Symbol: "\uE8EF")); items.Add(new LauncherItem("결과", $"{gv}", null, ("copy", $"{gv}"), Symbol: "\uE8EF")); return Task.FromResult>(items); } case "lcm": { var la = (long)Math.Abs(Math.Round(x)); var lb = (long)Math.Abs(Math.Round(y)); var g = GcdLong(la, lb); var lv = g == 0 ? 0 : la / g * lb; items.Add(new LauncherItem($"LCM({la}, {lb}) = {lv}", "최소공배수 · Enter 복사", null, ("copy", $"{lv}"), Symbol: "\uE8EF")); items.Add(new LauncherItem("결과", $"{lv}", null, ("copy", $"{lv}"), Symbol: "\uE8EF")); return Task.FromResult>(items); } case "mod": if (y == 0) { items.Add(ErrorItem("0으로 나눌 수 없습니다")); break; } return Result(items, $"{x} mod {y}", x % y, "mod"); } } // 파싱 실패 안내 items.Add(new LauncherItem($"알 수 없는 함수: '{fn}'", "calc sin/cos/tan/sqrt/log/ln/pow/factorial/gcd/lcm/abs/ceil/floor/round/deg/rad", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is ("copy", string text)) { try { System.Windows.Application.Current.Dispatcher.Invoke( () => Clipboard.SetText(text)); NotificationService.Notify("Calc", "클립보드에 복사했습니다."); } catch { } } return Task.CompletedTask; } // ── 헬퍼 ───────────────────────────────────────────────────────────────── private static Task> Result( List items, string label, double value, string fn, bool isAngle = false) { var formatted = FormatDouble(value); var extra = isAngle ? " °" : ""; items.Add(new LauncherItem($"{label} = {formatted}{extra}", $"· Enter 복사", null, ("copy", formatted), Symbol: "\uE8EF")); items.Add(new LauncherItem("결과", $"{formatted}{extra}", null, ("copy", formatted), Symbol: "\uE8EF")); // 추가 정보 if (!isAngle && value != 0) { items.Add(new LauncherItem("과학적 표기법", $"{value:E6}", null, ("copy", $"{value:E6}"), Symbol: "\uE8EF")); if (value > 0) items.Add(new LauncherItem("log₁₀ 값", $"{Math.Log10(value):F6}", null, null, Symbol: "\uE8EF")); } return Task.FromResult>(items); } private static string FormatDouble(double v) { if (double.IsNaN(v)) return "NaN"; if (double.IsPositiveInfinity(v)) return "+∞"; if (double.IsNegativeInfinity(v)) return "-∞"; // 정수라면 소수점 없이 if (v == Math.Floor(v) && Math.Abs(v) < 1e15) return $"{(long)v}"; return $"{v:G15}"; } private static LauncherItem BuildUsage(string title, string example) => new(title, example, null, null, Symbol: "\uE8EF"); private static LauncherItem ErrorItem(string msg) => new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783"); private static double DegToRad(double deg) => deg * Math.PI / 180.0; private static double RadToDeg(double rad) => rad * 180.0 / Math.PI; private static long Factorial(long n) { long r = 1; for (long i = 2; i <= n; i++) r *= i; return r; } private static long GcdLong(long a, long b) => b == 0 ? a : GcdLong(b, a % b); }