engine.js 全文)算命学ロジック完全仕様 の正となる実装。JavaScript(ブラウザ・Node 共用、1425行)。外部ライブラリには依存しない。
本編(共有ページ)
使い方(Node):
const SANMEI = require('./engine.js');
SANMEI.init(setsuTable, moonTable); // 付録A の JSON を渡す。moonTable は省略可(月相が null になる)
global.window = { HOLIDAY_TABLE: holidayTable }; // 祝日表(ブラウザでは window に置く)
const a = SANMEI.meishiki(1984, 10, 10, { hour: 14 });
| 行 | 名前 |
|---|---|
| 15 | KAN |
| 16 | SHI |
| 18 | GOGYO_KAN |
| 19 | GOGYO_SHI |
| 21 | INYO_KAN |
| 23 | SEI |
| 24 | sho |
| 25 | koku |
| 27 | SHUSEI_LIST |
| 28 | JUSEI_LIST |
| 31 | shusei |
| 44 | CHOSEI |
| 45 | UNJUN |
| 46 | JU_MAP |
| 53 | jusei |
| 70 | ZOKAN_KANTEI |
| 85 | ZOKAN_A |
| 94 | HONGEN |
| 96 | ZOKAN_TABLES |
| 98 | zokan |
| 106 | zokanStages |
| 114 | TENCHUSATSU |
| 116 | TENCHUSATSU_SHI |
| 122 | IJOU_KANSHI |
| 127 | IJOU_KANSHI_7 |
| 130 | ijouKanshi |
| 136 | KANGO |
| 139 | GOSO |
| 146 | jichu |
| 154 | jdn |
| 160 | fromJdn |
| 173 | WEEKDAY |
| 174 | weekday |
| 182 | SETSU_SHI |
| 183 | SETSU_NAME |
| 185 | GOTORA |
| 195 | init |
| 203 | initMoon |
| 220 | pad2 |
| 223 | moonPhaseOn |
| 230 | moonPhases |
| 249 | range |
| 254 | diffDays |
| 259 | assertInRange |
| 267 | findSetsu |
| 285 | findNextSetsu |
| 302 | kanshiIndex |
| 310 | kanshiYear |
| 325 | meishiki |
| 446 | daiun |
| 490 | SHIGO |
| 491 | CHU |
| 492 | SANGO |
| 493 | KEI3 |
| 494 | KEI2 |
| 495 | JIKEI |
| 496 | GAI |
| 497 | HA |
| 500 | isou |
| 514 | isouInner |
| 529 | suurihou |
| 552 | SHINKYO_STARS |
| 553 | SHINJAKU_STARS |
| 554 | shinkyo |
| 580 | RYOIKI |
| 586 | ryoikiOf |
| 588 | koudouRyoiki |
| 638 | ROKUSHIN_ORDER |
| 642 | TEIICHI |
| 647 | gyoOf |
| 648 | yinOf |
| 649 | mkKan |
| 650 | kangoOf |
| 651 | koibitoOf |
| 661 | rokushin |
| 738 | STAR_GOGYO |
| 751 | TEIICHI_POS |
| 766 | KYOUIKU_KANSHI |
| 779 | INSEI |
| 780 | ROSEI |
| 781 | KYOUIKU_PAIR |
| 805 | kyouikuSei |
| 823 | kyouiku |
| 853 | SETSUGETSU_SHI |
| 856 | ICHIRYU |
| 863 | SANRINBOU |
| 866 | TENSHA |
| 877 | kichijitsu |
| 900 | holidayTable |
| 908 | dayOfYear |
| 916 | holiday |
| 927 | hasHolidayData |
| 932 | teiichi |
| 962 | sainou |
| 1019 | REL_TEXT |
| 1028 | REL_MODE_TEXT |
| 1057 | TCS_MODE_TEXT |
| 1079 | AISHO_MODES |
| 1085 | TCS_TEXT |
| 1091 | ISOU_TEXT |
| 1102 | ROLE_TEXT |
| 1114 | SHINKYO_PAIR |
| 1126 | aisho |
| 1201 | gyoun |
| 1239 | HOUR_SLOTS |
| 1255 | gyounList |
| 1333 | search |
| 1368 | matches |
| 1383 | missing |
| 1395 | searchNearMiss |
/**
* 算命学 命式エンジン
*
* 設計上の注意:
* - 日付演算は Date を一切使わず、ユリウス日(JDN)の整数演算で行う。
* ブラウザのタイムゾーンに結果が左右されないため。
* - 節入りテーブル(setsu_table.json)は JST の日時。各暦年に12個で、
* 配列 index 0..11 が 1月..12月 の「節」に対応する。
* 1月=小寒(丑月) 2月=立春(寅月) 3月=啓蟄(卯月) ... 12月=大雪(子月)
* - 蔵干の取り方は流派差が大きいので variant で切り替えられるようにしている。
*/
const SANMEI = (function () {
'use strict';
const KAN = '甲乙丙丁戊己庚辛壬癸';
const SHI = '子丑寅卯辰巳午未申酉戌亥';
const GOGYO_KAN = { 甲: '木', 乙: '木', 丙: '火', 丁: '火', 戊: '土', 己: '土', 庚: '金', 辛: '金', 壬: '水', 癸: '水' };
const GOGYO_SHI = { 子: '水', 丑: '土', 寅: '木', 卯: '木', 辰: '土', 巳: '火', 午: '火', 未: '土', 申: '金', 酉: '金', 戌: '土', 亥: '水' };
// 1 = 陽, 0 = 陰
const INYO_KAN = { 甲: 1, 乙: 0, 丙: 1, 丁: 0, 戊: 1, 己: 0, 庚: 1, 辛: 0, 壬: 1, 癸: 0 };
const SEI = ['木', '火', '土', '金', '水']; // 相生の順
const sho = (a, b) => SEI[(SEI.indexOf(a) + 1) % 5] === b; // a が b を生じる
const koku = (a, b) => SEI[(SEI.indexOf(a) + 2) % 5] === b; // a が b を剋す
const SHUSEI_LIST = ['貫索星', '石門星', '鳳閣星', '調舒星', '禄存星', '司禄星', '車騎星', '牽牛星', '龍高星', '玉堂星'];
const JUSEI_LIST = ['天報星', '天印星', '天貴星', '天恍星', '天南星', '天禄星', '天将星', '天堂星', '天胡星', '天極星', '天庫星', '天馳星'];
/** 十大主星: 日干から見た対象干の関係で決まる */
function shusei(nikkan, target) {
const ea = GOGYO_KAN[nikkan];
const eb = GOGYO_KAN[target];
const same = INYO_KAN[nikkan] === INYO_KAN[target];
if (ea === eb) return same ? '貫索星' : '石門星';
if (sho(ea, eb)) return same ? '鳳閣星' : '調舒星';
if (koku(ea, eb)) return same ? '禄存星' : '司禄星';
if (koku(eb, ea)) return same ? '車騎星' : '牽牛星';
if (sho(eb, ea)) return same ? '龍高星' : '玉堂星';
throw new Error('主星判定に失敗: ' + nikkan + ' / ' + target);
}
// 十二運。陽干は長生位から順行、陰干は逆行。
const CHOSEI = { 甲: '亥', 乙: '午', 丙: '寅', 丁: '酉', 戊: '寅', 己: '酉', 庚: '巳', 辛: '子', 壬: '申', 癸: '卯' };
const UNJUN = ['長生', '沐浴', '冠帯', '建禄', '帝旺', '衰', '病', '死', '墓', '絶', '胎', '養'];
const JU_MAP = {
胎: ['天報星', 3], 養: ['天印星', 6], 長生: ['天貴星', 9], 沐浴: ['天恍星', 7],
冠帯: ['天南星', 10], 建禄: ['天禄星', 11], 帝旺: ['天将星', 12], 衰: ['天堂星', 8],
病: ['天胡星', 4], 死: ['天極星', 2], 墓: ['天庫星', 5], 絶: ['天馳星', 1],
};
/** 十二大従星: 日干と地支の十二運から決まる */
function jusei(nikkan, shi) {
const start = SHI.indexOf(CHOSEI[nikkan]);
const pos = SHI.indexOf(shi);
const step = INYO_KAN[nikkan] === 1 ? 1 : -1;
const idx = (((pos - start) * step) % 12 + 12) % 12;
const [name, energy] = JU_MAP[UNJUN[idx]];
return { name, energy, unsei: UNJUN[idx] };
}
// 蔵干テーブル。[経過日数の上限, 干] の並び。経過日数は節入り日を1日目として数える。
//
// KANTEI は kantei-app.com の出力を実測して起こしたもの(2000年の各月を1日刻みで観測)。
// 支によって段数が違うのがこの表の特徴:
// 1段(専気) … 子=癸, 卯=乙, 酉=辛
// 2段 … 午(己→丁), 亥(甲→壬) ← 初元を持たない
// 3段 … 残りの7支
// 土用系(丑辰未戌)は 10日/13日 で揃うが、四生(寅巳申)は支ごとに境界が違う。
const ZOKAN_KANTEI = {
子: [[99, '癸']],
丑: [[10, '癸'], [13, '辛'], [99, '己']],
寅: [[8, '戊'], [15, '丙'], [99, '甲']],
卯: [[99, '乙']],
辰: [[10, '乙'], [13, '癸'], [99, '戊']],
巳: [[6, '戊'], [15, '庚'], [99, '丙']],
午: [[20, '己'], [99, '丁']],
未: [[10, '丁'], [13, '乙'], [99, '己']],
申: [[11, '戊'], [14, '壬'], [99, '庚']],
酉: [[99, '辛']],
戌: [[10, '辛'], [13, '丁'], [99, '戊']],
亥: [[13, '甲'], [99, '壬']],
};
// 比較用に残している他流派の区分(四柱推命の月律分野蔵干に近いもの)
const ZOKAN_A = {
子: [[9, '壬'], [99, '癸']], 丑: [[9, '癸'], [12, '辛'], [99, '己']],
寅: [[7, '戊'], [14, '丙'], [99, '甲']], 卯: [[10, '甲'], [99, '乙']],
辰: [[9, '乙'], [12, '癸'], [99, '戊']], 巳: [[7, '戊'], [14, '庚'], [99, '丙']],
午: [[10, '丙'], [19, '己'], [99, '丁']], 未: [[9, '丁'], [12, '乙'], [99, '己']],
申: [[7, '戊'], [14, '壬'], [99, '庚']], 酉: [[10, '庚'], [99, '辛']],
戌: [[9, '辛'], [12, '丁'], [99, '戊']], 亥: [[7, '戊'], [14, '甲'], [99, '壬']],
};
// 本元(正気)のみ。年支・日支は本元固定で取る流派向け。
const HONGEN = { 子: '癸', 丑: '己', 寅: '甲', 卯: '乙', 辰: '戊', 巳: '丙', 午: '丁', 未: '己', 申: '庚', 酉: '辛', 戌: '戊', 亥: '壬' };
const ZOKAN_TABLES = { kantei: ZOKAN_KANTEI, A: ZOKAN_A };
function zokan(shi, keika, variant) {
if (variant === 'hongen') return HONGEN[shi];
const tbl = ZOKAN_TABLES[variant] || ZOKAN_KANTEI;
for (const [lim, k] of tbl[shi]) if (keika <= lim) return k;
return tbl[shi][tbl[shi].length - 1][1];
}
/** 蔵干の3段(初元・中元・本元)をそのまま返す。段を持たない支は null が入る。 */
function zokanStages(shi) {
const rows = ZOKAN_KANTEI[shi];
const kans = rows.map((r) => r[1]);
if (kans.length === 3) return { 初元: kans[0], 中元: kans[1], 本元: kans[2] };
if (kans.length === 2) return { 初元: null, 中元: kans[0], 本元: kans[1] };
return { 初元: null, 中元: null, 本元: kans[0] };
}
const TENCHUSATSU = ['戌亥天中殺', '申酉天中殺', '午未天中殺', '辰巳天中殺', '寅卯天中殺', '子丑天中殺'];
// 天中殺の名前 -> 該当する2支。行運(大運・年運・月運・日運・時運)の天中殺判定に使う。
const TENCHUSATSU_SHI = {
子丑天中殺: ['子', '丑'], 寅卯天中殺: ['寅', '卯'], 辰巳天中殺: ['辰', '巳'],
午未天中殺: ['午', '未'], 申酉天中殺: ['申', '酉'], 戌亥天中殺: ['戌', '亥'],
};
// 異常干支。13個説(通常6+暗合7)を既定にした。複数の解説サイトで一致する分類。
const IJOU_KANSHI = {
甲戌: '通常', 乙亥: '通常', 戊戌: '通常', 庚子: '通常', 辛亥: '通常', 丁巳: '通常',
辛巳: '暗合', 壬午: '暗合', 丁亥: '暗合', 丙戌: '暗合', 戊子: '暗合', 癸巳: '暗合', 己亥: '暗合',
};
// 朱学院系の7個説(参考用。甲午を含み、通常異常干支を採らない)
const IJOU_KANSHI_7 = ['甲午', '丁亥', '戊子', '己亥', '辛巳', '壬午', '癸巳'];
/** 干支が異常干支かどうか。set='13'(既定)or '7' */
function ijouKanshi(kanshi, set) {
if (set === '7') return IJOU_KANSHI_7.includes(kanshi) ? '異常干支' : null;
return IJOU_KANSHI[kanshi] || null;
}
// 干合(かんごう)の相手。伴星(人体図の左上「ご先祖様」)を出すのに使う。
const KANGO = { 甲: '己', 己: '甲', 乙: '庚', 庚: '乙', 丙: '辛', 辛: '丙', 丁: '壬', 壬: '丁', 戊: '癸', 癸: '戊' };
// 五鼠遁(日干 -> 子の刻の天干)。時柱を出すのに使う。
const GOSO = { 甲: '甲', 己: '甲', 乙: '丙', 庚: '丙', 丙: '戊', 辛: '戊', 丁: '庚', 壬: '庚', 戊: '壬', 癸: '壬' };
/**
* 時柱を求める。
* 時支は 23〜1時=子、1〜3時=丑 … の2時間区切り。
* 23時台を翌日の日干支で扱う流派(夜子時)もあるが、ここでは日付をまたがない扱いにしている。
*/
function jichu(nikkan, hour) {
const idx = Math.floor(((hour + 1) % 24) / 2);
const shi = SHI[idx];
const kan = KAN[(KAN.indexOf(GOSO[nikkan]) + idx) % 10];
return { kan, shi, kanshi: kan + shi, shiIndex: idx };
}
// ---- 日付ユーティリティ (Date は使わない) ----
function jdn(y, m, d) {
const a = Math.floor((14 - m) / 12);
const y2 = y + 4800 - a;
const m2 = m + 12 * a - 3;
return d + Math.floor((153 * m2 + 2) / 5) + 365 * y2 + Math.floor(y2 / 4) - Math.floor(y2 / 100) + Math.floor(y2 / 400) - 32045;
}
function fromJdn(j) {
const a = j + 32044;
const b = Math.floor((4 * a + 3) / 146097);
const c = a - Math.floor((146097 * b) / 4);
const d2 = Math.floor((4 * c + 3) / 1461);
const e = c - Math.floor((1461 * d2) / 4);
const m2 = Math.floor((5 * e + 2) / 153);
return [
100 * b + d2 - 4800 + Math.floor(m2 / 10),
m2 + 3 - 12 * Math.floor(m2 / 10),
e - Math.floor((153 * m2 + 2) / 5) + 1,
];
}
const WEEKDAY = ['月', '火', '水', '木', '金', '土', '日'];
function weekday(y, m, d) {
// JDN 2451545 (2000-01-01) は土曜。2451545 % 7 === 5 なので
// WEEKDAY を月曜起点で並べると jdn % 7 がそのまま曜日 index になる。
const j = jdn(y, m, d);
return WEEKDAY[((j % 7) + 7) % 7];
}
// 節入り index(0..11) -> 月支。1月=小寒=丑月 ... 12月=大雪=子月
const SETSU_SHI = ['丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子'];
const SETSU_NAME = ['小寒', '立春', '啓蟄', '清明', '立夏', '芒種', '小暑', '立秋', '白露', '寒露', '立冬', '大雪'];
// 五虎遁: 年干 -> 寅月の月干
const GOTORA = { 甲: '丙', 己: '丙', 乙: '戊', 庚: '戊', 丙: '庚', 辛: '庚', 丁: '壬', 壬: '壬', 戊: '甲', 癸: '甲' };
let TABLE = null;
let MOON = null; // [{jdn, hh, mm, kind}] 昇順
let MOON_BY_JDN = null; // jdn -> 月相(検索で毎日引くので Map にしておく)
/**
* @param {object} table 節入りテーブル(必須)
* @param {object} moonTable 新月・満月テーブル(省略可。渡さないと月相は使えない)
*/
function init(table, moonTable) {
TABLE = table;
MOON = null;
MOON_BY_JDN = null;
if (moonTable) initMoon(moonTable);
}
/** 差分エンコードされた月相テーブルを展開する */
function initMoon(table) {
const ep = table.epoch;
const epMin = jdn(ep[0], ep[1], ep[2]) * 1440;
MOON = [];
MOON_BY_JDN = new Map();
let cum = 0;
table.step.forEach((s, i) => {
cum += s;
const total = epMin + cum;
const j = Math.floor(total / 1440);
const rem = total - j * 1440;
const e = { jdn: j, hh: Math.floor(rem / 60), mm: rem % 60, kind: i % 2 === 0 ? '新月' : '満月' };
MOON.push(e);
MOON_BY_JDN.set(j, e); // 同じ日に新月と満月が両方来ることはない
});
}
const pad2 = (n) => String(n).padStart(2, '0');
/** その日が新月か満月か。該当しなければ null */
function moonPhaseOn(y, m, d) {
if (!MOON_BY_JDN) return null;
const e = MOON_BY_JDN.get(jdn(y, m, d));
return e ? { kind: e.kind, time: pad2(e.hh) + ':' + pad2(e.mm) } : null;
}
/** 期間内の新月・満月を列挙する */
function moonPhases(fromIso, toIso) {
if (!MOON) return [];
const [fy, fm, fd] = fromIso.split('-').map(Number);
const [ty, tm, td] = toIso.split('-').map(Number);
const j0 = jdn(fy, fm, fd);
const j1 = jdn(ty, tm, td);
return MOON.filter((e) => e.jdn >= j0 && e.jdn <= j1).map((e) => {
const [y, m, d] = fromJdn(e.jdn);
return {
date: y + '-' + pad2(m) + '-' + pad2(d),
time: pad2(e.hh) + ':' + pad2(e.mm),
kind: e.kind,
};
});
}
/**
* 計算できる年の範囲。1月上旬は前年12月の節(大雪)を参照するので、
* テーブル最初の年は完全には扱えない。min は 1 足した「安全に計算できる下限」を返す。
*/
function range() {
const years = Object.keys(TABLE).map(Number).sort((a, b) => a - b);
return { min: years[0] + 1, max: years[years.length - 1] };
}
/** 'YYYY-MM-DD' 同士の日数差 */
function diffDays(fromIso, toIso) {
const [fy, fm, fd] = fromIso.split('-').map(Number);
const [ty, tm, td] = toIso.split('-').map(Number);
return jdn(ty, tm, td) - jdn(fy, fm, fd);
}
function assertInRange(y) {
const r = range();
if (y < r.min || y > r.max) {
throw new Error('対応しているのは ' + r.min + '年〜' + r.max + '年です(節入りデータの範囲)。');
}
}
/** 対象日の直前の「節」を返す */
function findSetsu(y, m, d) {
const target = jdn(y, m, d);
let best = null;
for (const yy of [y - 1, y]) {
const rows = TABLE[String(yy)];
if (!rows) continue;
for (let i = 0; i < 12; i++) {
const [day, hh, mm] = rows[i];
const j = jdn(yy, i + 1, day);
if (j <= target && (best === null || j > best.jdn)) {
best = { jdn: j, year: yy, monthIndex: i, day, hh, mm, shi: SETSU_SHI[i], name: SETSU_NAME[i] };
}
}
}
return best;
}
/** 対象日より後の、直近の「節」を返す */
function findNextSetsu(y, m, d) {
const target = jdn(y, m, d);
let best = null;
for (const yy of [y, y + 1]) {
const rows = TABLE[String(yy)];
if (!rows) continue;
for (let i = 0; i < 12; i++) {
const j = jdn(yy, i + 1, rows[i][0]);
if (j > target && (best === null || j < best.jdn)) {
best = { jdn: j, year: yy, monthIndex: i, shi: SETSU_SHI[i], name: SETSU_NAME[i] };
}
}
}
return best;
}
/** 干支の文字から六十干支の通し番号(0始まり)を求める */
function kanshiIndex(kan, shi) {
const a = KAN.indexOf(kan);
const b = SHI.indexOf(shi);
for (let n = 0; n < 60; n++) if (n % 10 === a && n % 12 === b) return n;
throw new Error('ありえない干支の組み合わせ: ' + kan + shi);
}
/** 立春基準の干支年を返す */
function kanshiYear(y, m, d) {
const target = jdn(y, m, d);
const rows = TABLE[String(y)];
if (!rows) throw new Error('節入りテーブルに ' + y + ' 年がありません');
const risshun = jdn(y, 2, rows[1][0]);
return target >= risshun ? y : y - 1;
}
/**
* 命式を計算する
* @param {number} y 西暦年 (JST)
* @param {number} m 月
* @param {number} d 日
* @param {object} opts { zokanMonth, zokanYearDay } いずれも 'kantei' | 'A' | 'hongen'
*/
function meishiki(y, m, d, opts) {
opts = opts || {};
const vm = opts.zokanMonth || 'kantei';
const vyd = opts.zokanYearDay || 'kantei';
assertInRange(y);
const setsu = findSetsu(y, m, d);
if (!setsu) throw new Error('節入りが見つかりません: ' + y + '-' + m + '-' + d);
const target = jdn(y, m, d);
const keika = target - setsu.jdn + 1; // 節入り日を1日目とする
const ky = kanshiYear(y, m, d);
const yidx = ((ky - 4) % 60 + 60) % 60;
const ykan = KAN[yidx % 10];
const yshi = SHI[yidx % 12];
const mshi = setsu.shi;
const mkan = KAN[(KAN.indexOf(GOTORA[ykan]) + ((SHI.indexOf(mshi) - 2) % 12 + 12) % 12) % 10];
const didx = ((target + 49) % 60 + 60) % 60;
const dkan = KAN[didx % 10];
const dshi = SHI[didx % 12];
const zkY = zokan(yshi, keika, vyd);
const zkM = zokan(mshi, keika, vm);
const zkD = zokan(dshi, keika, vyd);
// 陽占(人体図)。配置は kantei-app.com と sanmei-stock.com(高尾式) の出力を実測して合わせた。
// 目上(北方)=年干 / 中心(中央)=月支の蔵干 / 目下(南方)=月干
// 社会(東方)=年支の蔵干 / 配偶者(西方)=日支の蔵干
// ご先祖様(左上)=伴星。日干と「年干の干合相手」の関係で出す。
const banseiKan = KANGO[ykan];
const stars = {
ancestor: { label: 'ご先祖様', pos: '左上', source: '年干' + ykan + 'の干合', kan: banseiKan, star: shusei(dkan, banseiKan) },
north: { label: '目上', pos: '北方', source: '年干', kan: ykan, star: shusei(dkan, ykan) },
center: { label: '中心', pos: '中央', source: '月支の蔵干', kan: zkM, star: shusei(dkan, zkM) },
south: { label: '目下', pos: '南方', source: '月干', kan: mkan, star: shusei(dkan, mkan) },
east: { label: '社会', pos: '東方', source: '年支の蔵干', kan: zkY, star: shusei(dkan, zkY) },
west: { label: '配偶者', pos: '西方', source: '日支の蔵干', kan: zkD, star: shusei(dkan, zkD) },
};
const juseis = {
early: Object.assign({ label: '初年運', source: '年支', shi: yshi }, jusei(dkan, yshi)),
mid: Object.assign({ label: '中年運', source: '月支', shi: mshi }, jusei(dkan, mshi)),
late: Object.assign({ label: '晩年運', source: '日支', shi: dshi }, jusei(dkan, dshi)),
};
const starSet = [...new Set(Object.values(stars).map((s) => s.star))];
const juseiSet = [...new Set(Object.values(juseis).map((s) => s.name))];
const totalEnergy = Object.values(juseis).reduce((a, s) => a + s.energy, 0);
// 主星の五行分布(参考値)
const STAR_GOGYO = { 貫索星: '木', 石門星: '木', 鳳閣星: '火', 調舒星: '火', 禄存星: '土', 司禄星: '土', 車騎星: '金', 牽牛星: '金', 龍高星: '水', 玉堂星: '水' };
const gogyoDist = {};
for (const s of Object.values(stars)) {
const g = STAR_GOGYO[s.star];
gogyoDist[g] = (gogyoDist[g] || 0) + 1;
}
return {
date: y + '-' + String(m).padStart(2, '0') + '-' + String(d).padStart(2, '0'),
weekday: weekday(y, m, d),
setsu: {
name: setsu.name,
jst: setsu.year + '-' + String(setsu.monthIndex + 1).padStart(2, '0') + '-' + String(setsu.day).padStart(2, '0')
+ ' ' + String(setsu.hh).padStart(2, '0') + ':' + String(setsu.mm).padStart(2, '0'),
keika,
isSetsuiriDay: keika === 1,
},
pillars: {
year: { kan: ykan, shi: yshi, kanshi: ykan + yshi, zokan: zkY, no: yidx + 1, stages: zokanStages(yshi) },
month: { kan: mkan, shi: mshi, kanshi: mkan + mshi, zokan: zkM, no: kanshiIndex(mkan, mshi) + 1, stages: zokanStages(mshi) },
day: { kan: dkan, shi: dshi, kanshi: dkan + dshi, zokan: zkD, no: didx + 1, stages: zokanStages(dshi) },
},
stars, starSet,
jusei: juseis, juseiSet, totalEnergy,
gogyoDist,
gogyoVariety: Object.keys(gogyoDist).length,
// 日柱から出る天中殺(本人の天中殺)。kantei-app では単に「天中殺」と表示される。
tenchusatsu: TENCHUSATSU[Math.floor(didx / 10)],
// 本人の天中殺に当たる2支。行運の天中殺判定に使う。
tenchusatsuShi: TENCHUSATSU_SHI[TENCHUSATSU[Math.floor(didx / 10)]],
// 各柱が異常干支かどうか(該当すれば '通常' か '暗合')
ijou: {
year: ijouKanshi(ykan + yshi, opts.ijouSet),
month: ijouKanshi(mkan + mshi, opts.ijouSet),
day: ijouKanshi(dkan + dshi, opts.ijouSet),
},
// その日が新月・満月か(月相テーブルを init に渡していない場合は null)
moon: moonPhaseOn(y, m, d),
// 時柱。opts.hour を渡したときだけ入る
hourPillar: (opts.hour == null) ? null : (function () {
const h = jichu(dkan, opts.hour);
return Object.assign({}, h, {
zokan: zokan(h.shi, keika, vyd),
stages: zokanStages(h.shi),
no: kanshiIndex(h.kan, h.shi) + 1,
ijou: ijouKanshi(h.kanshi, opts.ijouSet),
jusei: jusei(dkan, h.shi).name,
shusei: shusei(dkan, h.kan),
});
})(),
// 年柱から出る天中殺。kantei-app では「親の天中殺」と表示される。
tenchusatsuYear: TENCHUSATSU[Math.floor(yidx / 10)],
nikkanGogyo: GOGYO_KAN[dkan],
nisshiGogyo: GOGYO_SHI[dshi],
};
}
/**
* 大運を計算する。
*
* 進み方は「陽男陰女は順行、陰男陽女は逆行」(年干の陰陽と性別で決まる)。
* 立運(何歳から始まるか)は、順行なら次の節入りまでの日数、逆行なら前の節入りからの
* 日数を 3 で割った商。大運は月柱の次(逆行なら前)の干支から始まり、10年ごとに進む。
*
* @param {number} y 生年
* @param {number} m 生月
* @param {number} d 生日
* @param {'male'|'female'} gender 性別
* @param {object} opts { count: 何本出すか(既定12), zokanMonth, zokanYearDay }
*/
function daiun(y, m, d, gender, opts) {
opts = opts || {};
const count = opts.count || 12;
const a = meishiki(y, m, d, opts);
const male = gender === 'male' || gender === 1 || gender === '男' || gender === '男性';
const yangYear = INYO_KAN[a.pillars.year.kan] === 1;
const forward = yangYear === male; // 陽男・陰女 → 順行
const target = jdn(y, m, d);
const days = forward
? findNextSetsu(y, m, d).jdn - target // 次の節入りまで
: target - findSetsu(y, m, d).jdn; // 前の節入りから
// 3日を1年に換算する。端数は四捨五入(余り1なら切り捨て、余り2なら切り上げ)。
// 上限は10歳。節の間隔が32日ある月に生まれると計算上は11歳になるが、
// 大運が10年周期なのでそこで頭打ちになる(kantei-app の出力で確認)。
const startAge = Math.min(Math.round(days / 3), 10);
const mIdx = kanshiIndex(a.pillars.month.kan, a.pillars.month.shi);
const dkan = a.pillars.day.kan;
// 大運天中殺 = 大運の支が本人の天中殺の2支に入る期間。連続すると20年続く。
const honninShi = a.tenchusatsuShi;
const rows = [];
for (let i = 0; i < count; i++) {
const step = forward ? i + 1 : -(i + 1);
const idx = ((mIdx + step) % 60 + 60) % 60;
const kan = KAN[idx % 10];
const shi = SHI[idx % 12];
const age = startAge + i * 10;
const ju = jusei(dkan, shi);
rows.push({
age, year: y + age, kanshi: kan + shi, kan, shi,
shusei: shusei(dkan, kan), jusei: ju.name, energy: ju.energy,
// その大運の干支自体が属する旬から出る天中殺
tenchusatsu: TENCHUSATSU[Math.floor(idx / 10)],
// 本人にとってこの10年が大運天中殺かどうか
isDaiunTenchusatsu: honninShi.indexOf(shi) >= 0,
ijou: ijouKanshi(kan + shi, opts.ijouSet),
});
}
return { forward, direction: forward ? '順行' : '逆行', startAge, daysToSetsu: days, rows };
}
// ===== 位相法(支どうしの関係)=====
const SHIGO = { 子: '丑', 丑: '子', 寅: '亥', 亥: '寅', 卯: '戌', 戌: '卯', 辰: '酉', 酉: '辰', 巳: '申', 申: '巳', 午: '未', 未: '午' };
const CHU = { 子: '午', 午: '子', 丑: '未', 未: '丑', 寅: '申', 申: '寅', 卯: '酉', 酉: '卯', 辰: '戌', 戌: '辰', 巳: '亥', 亥: '巳' };
const SANGO = [['申', '子', '辰'], ['亥', '卯', '未'], ['寅', '午', '戌'], ['巳', '酉', '丑']];
const KEI3 = [['寅', '巳', '申'], ['丑', '戌', '未']];
const KEI2 = [['子', '卯']];
const JIKEI = ['辰', '午', '酉', '亥'];
const GAI = { 子: '未', 未: '子', 丑: '午', 午: '丑', 寅: '巳', 巳: '寅', 卯: '辰', 辰: '卯', 申: '亥', 亥: '申', 酉: '戌', 戌: '酉' };
const HA = { 子: '酉', 酉: '子', 丑: '辰', 辰: '丑', 寅: '亥', 亥: '寅', 卯: '午', 午: '卯', 巳: '申', 申: '巳', 未: '戌', 戌: '未' };
/** 支2つの関係を列挙する */
function isou(a, b) {
const out = [];
if (a === b) out.push(JIKEI.indexOf(a) >= 0 ? '自刑' : '比和');
if (SHIGO[a] === b) out.push('支合');
if (CHU[a] === b) out.push('冲');
for (const g of SANGO) if (a !== b && g.indexOf(a) >= 0 && g.indexOf(b) >= 0) out.push('半会');
for (const g of KEI3) if (a !== b && g.indexOf(a) >= 0 && g.indexOf(b) >= 0) out.push('刑');
for (const g of KEI2) if (a !== b && g.indexOf(a) >= 0 && g.indexOf(b) >= 0) out.push('刑');
if (GAI[a] === b) out.push('害');
if (HA[a] === b) out.push('破');
return out;
}
/** 命式の中の柱どうしの位相法 */
function isouInner(a) {
const y = a.pillars.year.shi, m = a.pillars.month.shi, d = a.pillars.day.shi;
return {
'日柱+月柱': isou(d, m),
'日柱+年柱': isou(d, y),
'月柱+年柱': isou(m, y),
};
}
// ===== 数理法・八門法 =====
/**
* 数理法。28元(天干3つ+各支の蔵干3段すべて)それぞれについて、
* 命式の3つの支との十二大従星の点数を出して合計する。干ごとに集計したものが数理法、
* 五行にまとめたものが八門法。kantei-app の出力と一致することを確認済み。
*/
function suurihou(y, m, d, opts) {
const a = (opts && opts.meishiki) || meishiki(y, m, d, opts);
const P = [a.pillars.year, a.pillars.month, a.pillars.day];
const shis = P.map((p) => p.shi);
const kans = P.map((p) => p.kan);
for (const p of P) {
for (const g of ['初元', '中元', '本元']) if (p.stages[g]) kans.push(p.stages[g]);
}
const byKan = {};
for (const k of KAN) byKan[k] = 0;
for (const k of kans) {
let sum = 0;
for (const sh of shis) sum += jusei(k, sh).energy;
byKan[k] += sum;
}
const byGogyo = { 木: 0, 火: 0, 土: 0, 金: 0, 水: 0 };
for (const k of KAN) byGogyo[GOGYO_KAN[k]] += byKan[k];
const total = Object.keys(byKan).reduce((acc, k) => acc + byKan[k], 0);
return { byKan, byGogyo, total, genList: kans };
}
// ===== 身強・身弱 =====
// 十二大従星のうち、外に出るエネルギーが大きい側を身強の星として数える。
const SHINKYO_STARS = ['天将星', '天禄星', '天南星', '天貴星'];
const SHINJAKU_STARS = ['天報星', '天極星', '天馳星', '天胡星'];
function shinkyo(a) {
const names = [a.jusei.early.name, a.jusei.mid.name, a.jusei.late.name];
const strong = names.filter((n) => SHINKYO_STARS.indexOf(n) >= 0).length;
const weak = names.filter((n) => SHINJAKU_STARS.indexOf(n) >= 0).length;
const e = a.totalEnergy;
return {
energy: e, strongCount: strong, weakCount: weak,
hasTenshosei: names.indexOf('天将星') >= 0,
label: e >= 25 ? '身強' : (e <= 14 ? '身弱' : '中庸'),
};
}
// ===== 行動領域(陰占チャート/宇宙盤)=====
/**
* 十二支を円周に置き、命式の3支(年支・月支・日支)を結んだ図のためのデータを返す。
* 3点が作る三角形の広さが「行動領域の広さ」にあたる。
* 配置は子を真上に置き、時計回りに30度ずつ(算命学で一般的な方位盤の並び)。
* sanmei-stock は同じ図をサーバー側で画像にしているが、向きまでは確認できていない。
*/
// 円周に並ぶのは十二支ではなく「六十干支の通し番号」。1が真上で、時計回りに60まで。
// 1番が6度おきに進むので、n番の角度は (n-1)×6 度。15番ずつで四つの領域に分かれる。
// 1〜15 右上=第一領域 / 16〜30 右下=第二領域 / 31〜45 左下=第三領域 / 46〜60 左上=第四領域
const RYOIKI = [
{ no: 1, name: '第一領域', title: '守備安定', desc: '知性的だが感情的でもある' },
{ no: 2, name: '第二領域', title: '伝達創造', desc: '情的で庶民性がある' },
{ no: 3, name: '第三領域', title: '攻撃開拓', desc: '庶民性と合理性を併せ持つ' },
{ no: 4, name: '第四領域', title: '習得知恵', desc: '合理的、理屈屋、知性豊か' },
];
const ryoikiOf = (no) => RYOIKI[Math.min(3, Math.floor((no - 1) / 15))];
function koudouRyoiki(a) {
const src = [
{ label: '年柱', pillar: a.pillars.year },
{ label: '月柱', pillar: a.pillars.month },
{ label: '日柱', pillar: a.pillars.day },
];
const pts = src.map((s) => {
const no = s.pillar.no; // 六十干支の通し番号(1〜60)
const deg = (no - 1) * 6; // 1番が真上、時計回り
const rad = (deg - 90) * Math.PI / 180;
return {
label: s.label, no, kanshi: s.pillar.kanshi, shi: s.pillar.shi, deg,
ryoiki: ryoikiOf(no), x: Math.cos(rad), y: Math.sin(rad),
};
});
// 三角形の面積(半径1の円に内接する三角形として)
const area = Math.abs(
(pts[1].x - pts[0].x) * (pts[2].y - pts[0].y) - (pts[2].x - pts[0].x) * (pts[1].y - pts[0].y)
) / 2;
// 円の面積(π)に対する割合。3点が重なるほど0に近づく
const ratio = area / Math.PI;
const unique = [...new Set(pts.map((p) => p.no))];
// 点どうしの角度差。いちばん離れている側が大きいほど広く散らばっている
const degs = [...new Set(pts.map((p) => p.deg))].sort((x, y) => x - y);
let maxGap = 0;
if (degs.length > 1) {
for (let i = 0; i < degs.length; i++) {
const g = (degs[(i + 1) % degs.length] - degs[i] + 360) % 360;
if (g > maxGap) maxGap = g;
}
}
let label;
if (unique.length === 1) label = '1点に集中(きわめて狭い)';
else if (unique.length === 2) label = '2点のみ(狭い)';
else if (ratio >= 0.35) label = '広い';
else if (ratio >= 0.18) label = 'ふつう';
else label = '狭い';
// 3柱がどの領域に入っているか
const spread = [...new Set(pts.map((p) => p.ryoiki.name))];
return { points: pts, area, ratio, unique: unique.length, maxGap, label, ryoiki: RYOIKI, spread };
}
// ===== 六親法(家系図)=====
// 算命学Stock の解説どおりの手順。観測221日 × 家族7人 = 1547項目すべてで出力と一致する。
// 母親 第一 正母(日干を生じる・陰陽が逆)→ 第二 偏母(陰陽が同じ)→ 定位置
// 父親 第一「実際に見つかった母の干」の干合相手 → 第二 その恋人の干 → 定位置
// 兄弟姉妹 代わりの干をとらない。日干のマスは「自分」なので数えない
// 配偶者 第一 日干の干合相手 → 第二 その恋人の干 → 定位置
// 子供 「実際に見つかった妻の干」が生じる干。妻と陰陽が同じなら女児、逆なら男児。定位置は使わない
// 「実際に見つかった干」から次を辿るのが要で、理想の干から辿ると合わない。
const ROKUSHIN_ORDER = ['母親', '父親', '兄弟', '姉妹', 'パートナー', '男子供', '女子供'];
// 陰占に決まっている家族の定位置。解説ページの本文と表が食い違っているが、
// 実例の記述も実際の出力も表のほうと一致する。
const TEIICHI = {
母親: ['year', '本元'], 父親: ['year', 'kan'], パートナー: ['day', '本元'],
家系: ['month', '本元'], 子供: ['month', 'kan'],
};
const gyoOf = (i) => Math.floor(i / 2); // 0木 1火 2土 3金 4水
const yinOf = (i) => i % 2; // 0陽 1陰
const mkKan = (g, y) => ((((g % 5) + 5) % 5) * 2 + y);
const kangoOf = (i) => (i + 5) % 10; // 干合の相手
const koibitoOf = (i) => { const p = kangoOf(i); return p % 2 === 0 ? p + 1 : p - 1; };
/**
* 六親法。日干から家族それぞれの「理想の干」を出し、
* 陰占(天干と蔵干)の中を第一優先→第二優先→定位置の順に探して「実際の干」を決める。
* どの段で見つかったかが、そのまま縁の深さの目安になる。
* 性別では結果が変わらない(女性42日ぶんでも同じ辿り方だった)。
*
* @param {object} a meishiki() の結果
*/
function rokushin(a) {
const dkan = a.pillars.day.kan;
const di = KAN.indexOf(dkan);
// 陰占にある干と、その居場所。日干のマスは「自分」なので家族には数えない。
const place = {};
const at = (kan, label) => { if (kan) (place[kan] = place[kan] || []).push(label); };
const PILLAR_LABEL = { year: '年', month: '月', day: '日' };
for (const key of ['year', 'month', 'day']) {
const p = a.pillars[key];
if (key !== 'day') at(p.kan, PILLAR_LABEL[key] + '干');
for (const g of ['初元', '中元', '本元']) at(p.stages[g], PILLAR_LABEL[key] + '支の' + g);
}
const has = (kan) => Object.prototype.hasOwnProperty.call(place, kan);
/** 定位置にいる干を返す */
const teiichiKan = (who) => {
const spec = TEIICHI[who];
if (!spec) return null;
const p = a.pillars[spec[0]];
return spec[1] === 'kan' ? p.kan : p.stages[spec[1]];
};
/** 候補を順に探し、見つからなければ定位置を見る */
const find = (cands, teiichiFor) => {
for (let i = 0; i < cands.length; i++) {
const k = KAN[cands[i]];
if (has(k)) return { real: k, level: i === 0 ? '理想どおり' : '第二優先' };
}
if (teiichiFor) {
const t = teiichiKan(teiichiFor);
if (t && has(t)) return { real: t, level: '定位置' };
}
return { real: null, level: 'なし' };
};
const out = {};
// 母親:正母がなければ偏母、それも無ければ母の定位置(年支の本元)
const seibo = mkKan(gyoOf(di) + 4, 1 - yinOf(di));
const henbo = mkKan(gyoOf(di) + 4, yinOf(di));
out['母親'] = Object.assign({ ideal: KAN[seibo] }, find([seibo, henbo], '母親'));
// 父親:実際に見つかった母の干合相手から辿る
const mBase = out['母親'].real ? KAN.indexOf(out['母親'].real) : null;
out['父親'] = mBase == null
? { ideal: KAN[kangoOf(seibo)], real: null, level: 'なし' }
: Object.assign({ ideal: KAN[kangoOf(seibo)] }, find([kangoOf(mBase), koibitoOf(mBase)], '父親'));
// 兄弟姉妹:代わりの干はとらない
const sis = mkKan(gyoOf(di), 1 - yinOf(di));
out['兄弟'] = { ideal: dkan, real: has(dkan) ? dkan : null, level: has(dkan) ? '理想どおり' : 'なし' };
out['姉妹'] = { ideal: KAN[sis], real: has(KAN[sis]) ? KAN[sis] : null, level: has(KAN[sis]) ? '理想どおり' : 'なし' };
out['パートナー'] = Object.assign({ ideal: KAN[kangoOf(di)] }, find([kangoOf(di), koibitoOf(di)], 'パートナー'));
// 子供:妻の干が生じる干。
// 解説では「日干が女性なら日干自身が妻」と書かれているが、実際の出力は性別によらず
// 配偶者の干から辿っていた(女性42日ぶんで確認)。出力に合わせて配偶者から辿る。
const wIdeal = kangoOf(di);
const wReal = out['パートナー'].real ? KAN.indexOf(out['パートナー'].real) : null;
for (const key of ['男子供', '女子供']) {
const girl = key === '女子供';
const ideal = KAN[mkKan(gyoOf(wIdeal) + 1, girl ? yinOf(wIdeal) : 1 - yinOf(wIdeal))];
out[key] = wReal == null
? { ideal, real: null, level: 'なし' }
: Object.assign({ ideal }, find([mkKan(gyoOf(wReal) + 1, girl ? yinOf(wReal) : 1 - yinOf(wReal))], null));
}
const rows = ROKUSHIN_ORDER.map((key) => Object.assign({
key,
star: shusei(dkan, out[key].ideal),
where: out[key].real ? (place[out[key].real] || []).join('・') : '',
}, out[key]));
return { self: dkan, rows };
}
// ===== 才能(剋数と才能星)=====
const STAR_GOGYO = { 貫索星: '木', 石門星: '木', 鳳閣星: '火', 調舒星: '火', 禄存星: '土', 司禄星: '土', 車騎星: '金', 牽牛星: '金', 龍高星: '水', 玉堂星: '水' };
/**
* 定位置(ていいち)を見る。
*
* 人体図の5つのマスには、それぞれ本来その星がいるべき五行が決まっている。
* そこに合う五行の星が入っていると「定位置にある」といい、その星は陽転しやすいとされる。
* 縦線(北・南)=精神星、横線(東・中央・西)=現実星
* 北方=水(龍高星・玉堂星)/南方=火(鳳閣星・調舒星)
* 東方=木(貫索星・石門星)/中央=土(禄存星・司禄星)/西方=金(車騎星・牽牛星)
*
* ご先祖様(伴星)は五つの定位置には含まれないので見ない。
*/
const TEIICHI_POS = {
north: { label: '目上(北方)', gogyo: '水', line: '精神', stars: ['龍高星', '玉堂星'] },
south: { label: '目下(南方)', gogyo: '火', line: '精神', stars: ['鳳閣星', '調舒星'] },
east: { label: '社会(東方)', gogyo: '木', line: '現実', stars: ['貫索星', '石門星'] },
center: { label: '中心(中央)', gogyo: '土', line: '現実', stars: ['禄存星', '司禄星'] },
west: { label: '配偶者(西方)', gogyo: '金', line: '現実', stars: ['車騎星', '牽牛星'] },
};
/* ===== 三大教育の干支 =====
甲戌・己未・癸丑。人を育てる・教育に関わる役割を持つ干支とされる。
※出典について:これは口伝として伝わるもので、公開されている資料では
裏を取れていない。算命学は口伝が本体で、web に出ない内容が多いため
「見つからない=誤り」とは言えないが、確立した定説として扱うのも違う。
画面では「口伝」と明記して出すこと。 */
const KYOUIKU_KANSHI = {
甲戌: '木の干が、戌という土に根を張る形。まっすぐに育てる。',
己未: '畑の土が、夏の温かい土に置かれる形。多くを慈しみ育てる。',
癸丑: '雨露が、冬の終わりの土を潤す形。じっくりと才能を育む。',
};
/* ===== 教育の星の組み合わせ =====
印星(学び・受信)と漏星=食傷(表現・発信)が揃うと、教えることに向くという読み。
印星 龍高星(偏印・体験で学ぶ)/玉堂星(印綬・書物で学ぶ)
漏星 鳳閣星(食神・自然体で伝える)/調舒星(傷官・感性で伝える)
四柱推命では、印星は「学問・インプット」、食傷は「本人から漏れ出るもの=表現・技術」
とされており、その定義自体は資料で確認できる。
下の2つの型は口伝として伝わっているもの。残り2通りは、星の意味から導いた派生。 */
const INSEI = ['龍高星', '玉堂星'];
const ROSEI = ['鳳閣星', '調舒星'];
const KYOUIKU_PAIR = {
'玉堂星+鳳閣星': {
name: '正統派の教育者', denshou: true,
note: '玉堂星で仕入れた正しい知識を、鳳閣星でわかりやすく自然に伝える。'
+ '学校の先生にいちばん多いとされる王道の組み合わせ。',
},
'龍高星+調舒星': {
name: '芸術・特殊分野の指導者', denshou: true,
note: '龍高星の体験から得た知恵と、調舒星の鋭い感性。'
+ '一対一で相手の才能を極限まで引き出す、職人や芸術の世界の師匠の型。',
},
'玉堂星+調舒星': {
name: '専門を掘り下げて伝える型', denshou: false,
note: '玉堂星の体系的な知識を、調舒星の感性で磨いて出す。'
+ '大人数に教えるより、書く・作る・批評するという形で伝わる。',
},
'龍高星+鳳閣星': {
name: '体験させて伝える型', denshou: false,
note: '龍高星が自分でやってみたことを、鳳閣星が構えずに渡す。'
+ '座学より、やらせてみて覚えさせる実践型の教え方が合う。',
},
};
/** 人体図の星から、教育の才能の組み合わせを見る */
function kyouikuSei(a) {
const has = (list) => list.filter((x) => a.starSet.indexOf(x) >= 0);
const insei = has(INSEI);
const rosei = has(ROSEI);
const pairs = [];
for (const i of insei) {
for (const r of rosei) {
const key = i + '+' + r;
if (KYOUIKU_PAIR[key]) pairs.push(Object.assign({ key, insei: i, rosei: r }, KYOUIKU_PAIR[key]));
}
}
return {
insei, rosei, pairs,
ok: insei.length > 0 && rosei.length > 0, // 学びと表現が両方そろっているか
};
}
/** 三柱(と時柱)のどこに教育の干支があるかを返す */
function kyouiku(a) {
const rows = [];
const check = (p, label) => {
if (p && KYOUIKU_KANSHI[p.kanshi]) {
rows.push({ pos: label, kanshi: p.kanshi, note: KYOUIKU_KANSHI[p.kanshi] });
}
};
check(a.pillars.year, '年柱');
check(a.pillars.month, '月柱');
check(a.pillars.day, '日柱');
check(a.hourPillar, '時柱');
return {
rows,
count: rows.length,
// 日柱にあるかどうかは別に見る(口伝では日柱が主とされる)
onDay: rows.some((r) => r.pos === '日柱'),
};
}
/* ===== 吉日・凶日(選日)=====
節月(立春からの月)と日の干支で機械的に決まるものだけを扱う。
六曜・不成就日は旧暦が要るので、ここには入れていない。
出典で確かめた規則
天赦日 春(立春〜立夏前日)=戊寅、夏=甲午、秋=戊申、冬=甲子
一粒万倍日 節月ごとに決まった二支(下の表)
三隣亡 節月1・4・7・10=亥、2・5・8・11=寅、3・6・9・12=午(凶日)
寅の日 日支が寅/巳の日 日支が巳/己巳の日 干支が己巳 */
// 節月(1=立春から)に対応する月支
const SETSUGETSU_SHI = ['寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', '子', '丑'];
// 一粒万倍日:節月ごとの二支
const ICHIRYU = [
['丑', '午'], ['酉', '寅'], ['子', '卯'], ['卯', '辰'],
['巳', '午'], ['酉', '午'], ['子', '未'], ['卯', '申'],
['酉', '午'], ['酉', '戌'], ['亥', '子'], ['卯', '子'],
];
// 三隣亡:節月ごとの支(凶日)
const SANRINBOU = ['亥', '寅', '午', '亥', '寅', '午', '亥', '寅', '午', '亥', '寅', '午'];
// 天赦日:季節ごとの干支。季節は立春・立夏・立秋・立冬で切る(=節月で数える)
const TENSHA = [
{ months: [1, 2, 3], kanshi: '戊寅', season: '春' },
{ months: [4, 5, 6], kanshi: '甲午', season: '夏' },
{ months: [7, 8, 9], kanshi: '戊申', season: '秋' },
{ months: [10, 11, 12], kanshi: '甲子', season: '冬' },
];
/**
* その日の吉日・凶日を出す。
* a は meishiki() の返り値。節月は月支から取る(立春からの数え方と一致する)。
*/
function kichijitsu(a) {
const setsugetsu = SETSUGETSU_SHI.indexOf(a.pillars.month.shi) + 1; // 1〜12
const dayShi = a.pillars.day.shi;
const dayKanshi = a.pillars.day.kanshi;
const good = [];
const bad = [];
const t = TENSHA.find((x) => x.months.indexOf(setsugetsu) >= 0);
if (t && t.kanshi === dayKanshi) good.push('天赦日');
if (setsugetsu >= 1 && ICHIRYU[setsugetsu - 1].indexOf(dayShi) >= 0) good.push('一粒万倍日');
if (dayShi === '寅') good.push('寅の日');
if (dayKanshi === '己巳') good.push('己巳の日');
else if (dayShi === '巳') good.push('巳の日');
if (setsugetsu >= 1 && SANRINBOU[setsugetsu - 1] === dayShi) bad.push('三隣亡');
return { setsugetsu, good, bad, all: good.concat(bad) };
}
/* ===== 祝日 =====
tools/gen_holiday_table.py が作った表を引く。1948年(祝日法の施行)以降だけ入っている。
春分・秋分は天体の運行で決まり、正式には前年に告示される。先の年は計算による見込み。 */
let HOLI = null;
function holidayTable() {
if (HOLI === null) {
HOLI = (typeof window !== 'undefined' && window.HOLIDAY_TABLE) || false;
}
return HOLI;
}
/** その年の通日(1月1日=1) */
function dayOfYear(y, m, d) {
return jdn(y, m, d) - jdn(y, 1, 1) + 1;
}
/**
* 祝日なら名前を返す。祝日でなければ null。
* 表を持っていない年(1948年より前など)も null を返す。
*/
function holiday(y, m, d) {
const t = holidayTable();
if (!t) return null;
const list = t.years[String(y)];
if (!list) return null;
const doy = dayOfYear(y, m, d);
for (const [n, idx] of list) if (n === doy) return t.names[idx];
return null;
}
/** 祝日の表がその年をカバーしているか */
function hasHolidayData(y) {
const t = holidayTable();
return !!(t && y >= t.from && y <= t.to);
}
function teiichi(a) {
const rows = ['north', 'south', 'east', 'center', 'west'].map((k) => {
const star = a.stars[k].star;
const t = TEIICHI_POS[k];
return {
key: k,
label: t.label,
line: t.line, // 精神/現実
gogyo: t.gogyo, // その位置の五行
home: t.stars, // そこに入るべき星
star: star, // 実際に入っている星
starGogyo: STAR_GOGYO[star],
ok: t.stars.indexOf(star) >= 0, // 定位置に在るか
};
});
const hit = rows.filter((r) => r.ok);
return {
rows: rows,
count: hit.length,
keys: hit.map((r) => r.key),
spirit: rows.filter((r) => r.line === '精神' && r.ok).length, // 精神星のうち定位置の数
real: rows.filter((r) => r.line === '現実' && r.ok).length, // 現実星のうち定位置の数
};
}
/**
* 才能星を出す。
* 人体図の主星6つ(伴星を含む)について、その星の五行が他の主星をいくつ剋すかを数え、
* 一番多いものを才能星とする。sanmei-stock の出力と一致することを確認済み。
*/
function sainou(a) {
// 剋数は伴星(ご先祖様)を除いた主星5つの中で数える。
// 「自分が剋す数」と「自分が剋される数」の合計。
const keys = ['east', 'west', 'center', 'north', 'south']; // 東・西・中央・北・南の順
const list = keys.map((k) => ({ pos: a.stars[k].label, star: a.stars[k].star, gogyo: STAR_GOGYO[a.stars[k].star] }));
for (const r of list) {
const gives = list.filter((o) => o !== r && koku(r.gogyo, o.gogyo)).length;
const takes = list.filter((o) => o !== r && koku(o.gogyo, r.gogyo)).length;
r.kokusu = gives + takes;
}
const candidates = list;
const max = candidates.reduce((m, r) => Math.max(m, r.kokusu), 0);
// 型:東・西・中央・北・南(社会・配偶者・中心・目上・目下)の剋数の並びで決まる。
// sanmei-stock の出力22件を観測して起こした。
const k = list.map((r) => r.kokusu); // list は東・西・中央・北・南の順に作ってある
// 型の判定。sanmei-stock の出力86件を観測して起こし、全件一致を確認した。
// 全部0 → 無玄 / 全部同じ → 五宮同均
// 中央が単独で最大 → 了殿
// 中央以外が単独で最大かつ次点と2以上の差 → 点欽
// それ以外 → 順殿
const kMax = Math.max.apply(null, k);
const kMin = Math.min.apply(null, k);
const maxCount = k.filter((x) => x === kMax).length;
const lower = k.filter((x) => x < kMax);
const second = lower.length ? Math.max.apply(null, lower) : kMax;
let kata;
if (kMax === 0) kata = '無玄の型';
else if (kMax === kMin) kata = '五宮同均の型';
else if (k[2] === kMax && maxCount === 1) kata = '了殿の型';
else if (maxCount === 1 && (kMax - second) >= 2) kata = '点欽の型';
else kata = '順殿の型';
// 運:水(龍高・玉堂)と火(鳳閣・調舒)の星がどの方位にあるかで決まる。
// 中央にあれば北天運。無ければ南天運。東西だけなら東天運、南北だけなら西天運、
// 両方にまたがれば北天運。sanmei-stock の出力176件で全一致を確認。
const isThink = (g) => g === '水' || g === '火';
const eastWest = isThink(list[0].gogyo) || isThink(list[1].gogyo); // 社会・配偶者
const center = isThink(list[2].gogyo);
const northSouth = isThink(list[3].gogyo) || isThink(list[4].gogyo); // 目上・目下
let un;
if (center) un = '北天運';
else if (!eastWest && !northSouth) un = '南天運';
else if (eastWest && northSouth) un = '北天運';
else if (eastWest) un = '東天運';
else un = '西天運';
return {
list, max, kata, un,
kokusuOrder: list.map((r) => ({ pos: r.pos, kokusu: r.kokusu })),
talent: [...new Set(candidates.filter((r) => r.kokusu === max && max > 0).map((r) => r.star))],
};
}
// ===== 相性の読み方(機械的な固定文)=====
// AI に書かせると学習データ由来の作り話が混ざるので、判定結果に対する説明は
// すべてここに固定文として持ち、組み合わせで出す。
const REL_TEXT = {
同質: { short: '同', label: '同質', desc: '似た者同士。考え方が近く話が早い。ただし役割がかぶりやすく、同じ場面で競合しやすい' },
育てる: { short: '育', label: '育てる', desc: 'あなたが相手に与える側。教える・任せる・引き上げると活きる。与えすぎると自分が消耗しやすい' },
支えられる: { short: '受', label: '支えられる', desc: '相手があなたを支える側。頼る・学ぶと回る。遠慮すると関係が動きにくい' },
動かす: { short: '推', label: '動かす', desc: 'あなたが相手を引き締める側。依頼や要求が通りやすい。強く出すぎると相手が萎縮する' },
動かされる: { short: '被', label: '動かされる', desc: '相手があなたを動かす側。課題や刺激をもらえる。圧に感じたら距離と頻度を調整する' },
};
// 同じ関係でも、何の間柄かで気をつける所が変わる。かたち別×間柄別の言い方。
const REL_MODE_TEXT = {
同質: {
business: '役割を明確に分けないと同じ仕事を取り合う。担当を先に線引きし、決裁者をどちらかに寄せると回る',
love: '価値観が近く一緒にいて楽。ただし似た弱点も重なるので、苦手なことを外に任せる工夫がいる',
friend: '趣味も間合いも合いやすい。長く続くが、同じ相手とばかり組むと世界が広がりにくい',
},
育てる: {
business: 'あなたが出資・発注・教育の側に回ると噛み合う。見返りの取り決めを先に決めないと持ち出しになる',
love: 'あなたが尽くす形になりやすい。相手の成長は早いが、無理をして与え続けると急に冷める',
friend: '相談される側になりやすい。頼られるのは信頼の証だが、聞き役だけにならないよう自分の話もする',
},
支えられる: {
business: '相手を後ろ盾や仕入れ元に据えると強い。任せきりにせず、成果と情報はこまめに返すこと',
love: '甘えられる相手。遠慮すると関係が進まないので、素直に頼るほうがうまくいく',
friend: '困ったときに助けてくれる相手。もらう一方にならないよう、別の形で返すと長続きする',
},
動かす: {
business: '発注側・管理側が向く。指示は通るが、細かく詰めすぎると相手が動けなくなる',
love: '主導権を持ちやすい。決めるのが早い反面、相手の言い分を聞く時間を意識して取ること',
friend: 'あなたが誘って動き出す関係。強く押しすぎると相手が黙って離れるので、断る余地を残す',
},
動かされる: {
business: '相手の要求で鍛えられる。条件と納期を明文化しておかないと、こちらの負担が膨らむ',
love: '刺激は多いが振り回されやすい。会う頻度と連絡の間隔を自分で決めておくと保つ',
friend: '背中を押してくれる相手。しんどいときは距離を取ってよい、と最初から思っておく',
},
};
// 天中殺の関わり方も、間柄で意味が変わる
const TCS_MODE_TEXT = {
same: {
business: '同じ時期に同じ弱点が出る。二人だけで事業を回さず、天中殺の年をずらせる第三者を入れておく',
love: '価値観が深く合う。ただし停滞期が重なるので、その時期に大きな決断をしないと決めておく',
friend: '一緒にいて楽な相手。落ち込む時期も重なるので、互いに引きずられないよう気をつける',
},
bInA: {
business: '相手に惹かれて条件を甘くしがち。金銭と契約は必ず書面にし、第三者に見てもらうこと',
love: '強く惹かれるが、振り回されやすい組み合わせ。冷静な時期に条件をすり合わせておく',
friend: '面白い相手だが、こちらのペースが崩れやすい。付き合う頻度は自分で決めておく',
},
aInB: {
business: '相手があなたに寄りかかってくる。頼られる範囲を先に決めておかないと抱え込む',
love: '相手から強く求められる関係。応えきれない部分は早めに伝えたほうが後が楽',
friend: '相手にとって特別な存在になりやすい。無理な期待には線を引いてよい',
},
none: {
business: '天中殺の絡みはない。条件と役割で判断できる、扱いやすい間柄',
love: '天中殺の絡みはない。過度な引力も反発もなく、落ち着いて進められる',
friend: '天中殺の絡みはない。距離感を保ちやすく、長く付き合いやすい',
},
};
const AISHO_MODES = [
{ key: 'general', label: '一般' },
{ key: 'business', label: '事業' },
{ key: 'love', label: '恋愛' },
{ key: 'friend', label: '友人' },
];
const TCS_TEXT = {
same: '2人とも同じ天中殺。運気の谷が同じ時期に来るので、その2年は2人だけで大きな決断をしない方がよい',
bInA: '相手の日支があなたの天中殺に当たる。惹かれやすい一方で振り回されやすい',
aInB: 'あなたの日支が相手の天中殺に当たる。相手から見て掴みどころのない存在になりやすい',
none: '天中殺の重なりはなし',
};
const ISOU_TEXT = {
支合: '引き合う関係。自然と距離が近くなる',
半会: '同じ方向を向きやすい。共同作業に向く',
冲: 'ぶつかりやすいが動きも生まれる。緊張感のある関係',
刑: '摩擦が起きやすい。役割と責任をはっきり決めておくとよい',
害: '細かいすれ違いが起きやすい。こまめに確認するとよい',
破: '途中で崩れやすい。節目ごとに合意を取り直すとよい',
比和: '同じ支どうし。共感しやすいが視点も重なる',
自刑: '同じ支どうしで、こだわりが強く出やすい組み合わせ',
};
// 中心(中央)の星から見た、チームでの役割
const ROLE_TEXT = {
貫索星: '一人で貫く力。自分の領域を任せると強い',
石門星: '人を束ねる力。チームをまとめる位置が向く',
鳳閣星: '場を和ませ、伝える力。対外的な窓口や広報に向く',
調舒星: '繊細な感性。細部を詰める、作品や文章を作る役に向く',
禄存星: '人と縁を集める力。営業や世話役、資金を回す役に向く',
司禄星: 'こつこつ積み上げる力。実務・管理・経理に向く',
車騎星: '前に出て動かす力。実行部隊、突破口を開く役に向く',
牽牛星: '責任感と体面。組織の顔、対外的な折衝や調整に向く',
龍高星: '好奇心と創造。新しい仕組みを作る、企画・開発に向く',
玉堂星: '知性と理論。分析、教育、仕組みの言語化に向く',
};
const SHINKYO_PAIR = {
'身強×身強': 'どちらも押しが強い。担当領域を分けると衝突しにくい',
'身強×身弱': '押す人と受ける人。役割が自然に分かれやすい',
'身弱×身強': '押す人と受ける人。役割が自然に分かれやすい',
'身弱×身弱': 'どちらも内に向く。決める人を先に決めておくと進む',
};
// ===== 相性 =====
/**
* 2人の相性を、事実として計算できる項目だけ並べる。
* 良し悪しの判定はせず、関係の内容(五行・天中殺・位相法・共通の星など)を返す。
*/
function aisho(meA, meB, mode) {
const ka = meA.pillars.day.kan, kb = meB.pillars.day.kan;
const ea = GOGYO_KAN[ka], eb = GOGYO_KAN[kb];
let gogyoRel;
if (ea === eb) gogyoRel = '比和(同じ五行)';
else if (sho(ea, eb)) gogyoRel = 'あなたが相手を生む(' + ea + '生' + eb + ')';
else if (sho(eb, ea)) gogyoRel = '相手があなたを生む(' + eb + '生' + ea + ')';
else if (koku(ea, eb)) gogyoRel = 'あなたが相手を剋す(' + ea + '剋' + eb + ')';
else gogyoRel = '相手があなたを剋す(' + eb + '剋' + ea + ')';
const starsA = new Set(Object.keys(meA.stars).map((k) => meA.stars[k].star));
const starsB = new Set(Object.keys(meB.stars).map((k) => meB.stars[k].star));
const shared = [...starsA].filter((x) => starsB.has(x));
const sa = shinkyo(meA), sb = shinkyo(meB);
const pos = {
'日柱どうし': isou(meA.pillars.day.shi, meB.pillars.day.shi),
'月柱どうし': isou(meA.pillars.month.shi, meB.pillars.month.shi),
'年柱どうし': isou(meA.pillars.year.shi, meB.pillars.year.shi),
};
const posAll = [].concat(pos['日柱どうし'], pos['月柱どうし'], pos['年柱どうし']);
// 五行の関係を「かたち」に落とす
let kind;
if (ea === eb) kind = '同質';
else if (sho(ea, eb)) kind = '育てる';
else if (sho(eb, ea)) kind = '支えられる';
else if (koku(ea, eb)) kind = '動かす';
else kind = '動かされる';
const tcsKey = (meA.tenchusatsu === meB.tenchusatsu) ? 'same'
: (meA.tenchusatsuShi.indexOf(meB.pillars.day.shi) >= 0 ? 'bInA'
: (meB.tenchusatsuShi.indexOf(meA.pillars.day.shi) >= 0 ? 'aInB' : 'none'));
const uniqIsou = [...new Set(posAll)];
const skPair = shinkyo(meA).label.replace('中庸', '身強') + '×' + shinkyo(meB).label.replace('中庸', '身強');
return {
kind, kindText: REL_TEXT[kind],
// 読んでそのまま使える説明文(固定文の組み合わせ。AI は使っていない)
reading: [
REL_TEXT[kind].desc,
// 間柄を選んでいるときは、その間柄ならではの見どころを足す
(mode && mode !== 'general' && REL_MODE_TEXT[kind][mode]) || '',
TCS_TEXT[tcsKey],
(mode && mode !== 'general' && TCS_MODE_TEXT[tcsKey] && TCS_MODE_TEXT[tcsKey][mode]) || '',
SHINKYO_PAIR[skPair] || '',
uniqIsou.map((x) => ISOU_TEXT[x]).filter(Boolean).join('。'),
shared.length ? '「' + shared.join('・') + '」を2人とも持っているので、その面では話が通じやすい'
: '持ち味が重ならないので、補い合う形になりやすい',
].filter(Boolean),
gogyo: { a: ea, b: eb, relation: gogyoRel, same: ea === eb },
tenchusatsu: {
a: meA.tenchusatsu, b: meB.tenchusatsu, same: meA.tenchusatsu === meB.tenchusatsu,
// 相手の日支が自分の天中殺に入っているか
bInA: meA.tenchusatsuShi.indexOf(meB.pillars.day.shi) >= 0,
aInB: meB.tenchusatsuShi.indexOf(meA.pillars.day.shi) >= 0,
},
sharedStars: shared,
shinkyo: { a: sa, b: sb },
isou: pos,
isouSummary: posAll.length ? [...new Set(posAll)].join('・') : 'なし',
kango: KANGO[ka] === kb, // 日干どうしが干合するか
};
}
/**
* 行運(年運・月運・日運・時運)を出す。
*
* 本人の日干を基準に、巡ってきた干支との関係で主星・従星を出す。
* 天中殺は「本人の天中殺に当たる2支が巡ってきたとき」に成立する。
* 年運は干支年(立春区切り)、月運は節入り区切りで見る。
*
* @param {object} birth {y, m, d} 生年月日
* @param {object} at {y, m, d, hour} 見たい日時(hour は省略可)
*/
function gyoun(birth, at, opts) {
opts = opts || {};
const me = meishiki(birth.y, birth.m, birth.d, opts);
const target = meishiki(at.y, at.m, at.d, opts);
const dkan = me.pillars.day.kan; // 星は本人の日干から見る
const mine = me.tenchusatsuShi;
const row = (label, kan, shi) => {
const ju = jusei(dkan, shi);
return {
label, kanshi: kan + shi, kan, shi,
shusei: shusei(dkan, kan),
jusei: ju.name,
energy: ju.energy,
unsei: ju.unsei,
isTenchusatsu: mine.indexOf(shi) >= 0,
ijou: ijouKanshi(kan + shi, opts.ijouSet),
};
};
const out = {
tenchusatsu: me.tenchusatsu,
shi: mine,
nikkan: dkan,
moon: moonPhaseOn(at.y, at.m, at.d),
year: row('年運', target.pillars.year.kan, target.pillars.year.shi),
month: row('月運', target.pillars.month.kan, target.pillars.month.shi),
day: row('日運', target.pillars.day.kan, target.pillars.day.shi),
hour: null,
};
if (at.hour != null) {
const h = jichu(target.pillars.day.kan, at.hour);
out.hour = row('時運', h.kan, h.shi);
}
return out;
}
// 時運の12コマ。jichu の区切りに合わせた代表の時刻と範囲。
const HOUR_SLOTS = [
[23, '23:00〜00:59'], [1, '01:00〜02:59'], [3, '03:00〜04:59'], [5, '05:00〜06:59'],
[7, '07:00〜08:59'], [9, '09:00〜10:59'], [11, '11:00〜12:59'], [13, '13:00〜14:59'],
[15, '15:00〜16:59'], [17, '17:00〜18:59'], [19, '19:00〜20:59'], [21, '21:00〜22:59'],
];
/**
* 行運を一覧で返す。大運と同じように並べて追えるようにするためのもの。
*
* @param {{y:number,m:number,d:number}} birth 本人の生年月日
* @param {'year'|'month'|'day'|'hour'} unit どの単位で並べるか
* @param {{y:number,m:number,d:number}} anchor 起点。年運はこの年から、月運はこの年の12か月、
* 日運はこの月、時運はこの日を並べる
* @param {object} [opts] count で年運の年数を指定(既定12)。
* now を渡すと「いまの行」の印はそちらと突き合わせる(前後に送ったとき用)
*/
function gyounList(birth, unit, anchor, opts) {
opts = opts || {};
const now = opts.now || anchor;
const me = meishiki(birth.y, birth.m, birth.d, opts);
const dkan = me.pillars.day.kan;
const mine = me.tenchusatsuShi;
const row = (label, kan, shi, extra) => {
const ju = jusei(dkan, shi);
return Object.assign({
label, kanshi: kan + shi, kan, shi,
shusei: shusei(dkan, kan),
jusei: ju.name, energy: ju.energy, unsei: ju.unsei,
isTenchusatsu: mine.indexOf(shi) >= 0,
ijou: ijouKanshi(kan + shi, opts.ijouSet),
}, extra || {});
};
/** その年の誕生日を迎えたあとの年齢 */
const ageAt = (y) => y - birth.y;
const lim = range();
const clamp = (y) => Math.min(lim.max, Math.max(lim.min, y));
const rows = [];
if (unit === 'year') {
// 年干支は立春で替わるので、立春を過ぎた6月で読む
const count = opts.count || 12;
const from = clamp(anchor.y);
const to = clamp(from + count - 1);
for (let y = from; y <= to; y++) {
const a = meishiki(y, 6, 15, opts);
rows.push(row(y + '年', a.pillars.year.kan, a.pillars.year.shi,
{ year: y, age: ageAt(y), isNow: y === now.y }));
}
} else if (unit === 'month') {
// 月干支は節入りで替わる。各月の15日はその節の中に確実に入る
for (let m = 1; m <= 12; m++) {
const a = meishiki(anchor.y, m, 15, opts);
rows.push(row(anchor.y + '年' + m + '月', a.pillars.month.kan, a.pillars.month.shi,
{ year: anchor.y, month: m, isNow: anchor.y === now.y && m === now.m }));
}
} else if (unit === 'day') {
const last = new Date(Date.UTC(anchor.y, anchor.m, 0)).getUTCDate();
const sameMonth = anchor.y === now.y && anchor.m === now.m;
for (let d = 1; d <= last; d++) {
const a = meishiki(anchor.y, anchor.m, d, opts);
const ki = kichijitsu(a);
rows.push(row(anchor.y + '年' + anchor.m + '月' + d + '日(' + a.weekday + ')',
a.pillars.day.kan, a.pillars.day.shi,
{
year: anchor.y, month: anchor.m, day: d, isNow: sameMonth && d === now.d, moon: a.moon,
kichi: ki.good, kyou: ki.bad,
holiday: holiday(anchor.y, anchor.m, d),
}));
}
} else if (unit === 'hour') {
const a = meishiki(anchor.y, anchor.m, anchor.d, opts);
for (const [h, range] of HOUR_SLOTS) {
const j = jichu(a.pillars.day.kan, h);
rows.push(row(j.shi + '(' + range + ')', j.kan, j.shi, { hour: h, range }));
}
}
return { unit, rows, nikkan: dkan, tenchusatsu: me.tenchusatsu, shi: mine, anchor };
}
/**
* 条件に合う日を検索する
* @param {string} from 'YYYY-MM-DD'
* @param {string} to 'YYYY-MM-DD'
* @param {object} cond {
* tenchusatsu: string[]|null, // いずれかに一致 (OR)
* nikkanGogyo: string[]|null, // 日干の五行がいずれかに一致 (OR)
* nisshiGogyo: string[]|null, // 日支の五行がいずれかに一致 (OR)
* requireShusei: string[], // 主星に全部含む (AND)
* requireJuseiAny: string[], // 従星にいずれか含む (OR)
* minEnergy: number|null, // 従星エネルギー合計の下限
* robust: boolean // trueなら蔵干4パターン全部で成立する日だけ返す
* }
*/
function search(from, to, cond, opts) {
cond = cond || {};
opts = opts || {};
const [fy, fm, fd] = from.split('-').map(Number);
const [ty, tm, td] = to.split('-').map(Number);
const j0 = jdn(fy, fm, fd);
const j1 = jdn(ty, tm, td);
const lim = range();
// 既定は kantei(実測に合わせた表)。robust のときは他流派の区分でも成立するものだけに絞る。
// ここを 'A' のままにしていたため、検索だけ別の蔵干で計算されるバグがあった。
const variants = cond.robust
? [['kantei', 'kantei'], ['A', 'A'], ['kantei', 'hongen'], ['A', 'hongen']]
: [[opts.zokanMonth || 'kantei', opts.zokanYearDay || 'kantei']];
const hits = [];
const maxHits = opts.maxHits || 500;
for (let j = j0; j <= j1; j++) {
const [y, m, d] = fromJdn(j);
if (y < lim.min || y > lim.max) continue;
let ok = true;
let base = null;
for (const [vm, vyd] of variants) {
const a = meishiki(y, m, d, { zokanMonth: vm, zokanYearDay: vyd });
if (base === null) base = a;
if (!matches(a, cond)) { ok = false; break; }
}
if (ok && base) {
hits.push(base);
if (hits.length >= maxHits) break;
}
}
return hits;
}
function matches(a, cond) {
if (cond.tenchusatsu && cond.tenchusatsu.length && !cond.tenchusatsu.includes(a.tenchusatsu)) return false;
if (cond.nikkanGogyo && cond.nikkanGogyo.length && !cond.nikkanGogyo.includes(a.nikkanGogyo)) return false;
if (cond.nisshiGogyo && cond.nisshiGogyo.length && !cond.nisshiGogyo.includes(a.nisshiGogyo)) return false;
if (cond.requireShusei && cond.requireShusei.length) {
for (const s of cond.requireShusei) if (!a.starSet.includes(s)) return false;
}
if (cond.requireJuseiAny && cond.requireJuseiAny.length) {
if (!cond.requireJuseiAny.some((s) => a.juseiSet.includes(s))) return false;
}
if (cond.minEnergy != null && a.totalEnergy < cond.minEnergy) return false;
return true;
}
/** ある日について、指定条件のうち何が欠けているかを返す(惜しい日の説明用) */
function missing(a, cond) {
const miss = [];
if (cond.tenchusatsu && cond.tenchusatsu.length && !cond.tenchusatsu.includes(a.tenchusatsu)) miss.push('天中殺');
if (cond.nikkanGogyo && cond.nikkanGogyo.length && !cond.nikkanGogyo.includes(a.nikkanGogyo)) miss.push('日干の五行');
if (cond.nisshiGogyo && cond.nisshiGogyo.length && !cond.nisshiGogyo.includes(a.nisshiGogyo)) miss.push('日支の五行');
for (const s of (cond.requireShusei || [])) if (!a.starSet.includes(s)) miss.push(s);
if (cond.requireJuseiAny && cond.requireJuseiAny.length && !cond.requireJuseiAny.some((s) => a.juseiSet.includes(s))) miss.push('従星');
if (cond.minEnergy != null && a.totalEnergy < cond.minEnergy) miss.push('エネルギー');
return miss;
}
/** 条件を1つだけ落とせば成立する「惜しい日」を探す */
function searchNearMiss(from, to, cond, opts) {
opts = opts || {};
const [fy, fm, fd] = from.split('-').map(Number);
const [ty, tm, td] = to.split('-').map(Number);
const lim = range();
const out = [];
for (let j = jdn(fy, fm, fd); j <= jdn(ty, tm, td); j++) {
const [y, m, d] = fromJdn(j);
if (y < lim.min || y > lim.max) continue;
const a = meishiki(y, m, d, { zokanMonth: opts.zokanMonth || 'kantei', zokanYearDay: opts.zokanYearDay || 'kantei' });
const miss = missing(a, cond);
if (miss.length === 1) out.push({ meishiki: a, missing: miss[0] });
if (out.length >= (opts.maxHits || 200)) break;
}
return out;
}
return {
init, range, diffDays, meishiki, daiun, gyoun, gyounList, moonPhaseOn, moonPhases, ijouKanshi, jichu,
suurihou, isou, isouInner, shinkyo, aisho, sainou, rokushin, koudouRyoiki, teiichi,
holiday, hasHolidayData, kichijitsu, kyouiku, kyouikuSei,
search, searchNearMiss, missing, matches,
KAN, SHI, SHUSEI_LIST, JUSEI_LIST, TENCHUSATSU,
GOGYO_KAN, GOGYO_SHI, JU_MAP, TENCHUSATSU_SHI, IJOU_KANSHI, KANGO, TEIICHI_POS, KYOUIKU_KANSHI,
REL_TEXT, ISOU_TEXT, ROLE_TEXT, TCS_TEXT, REL_MODE_TEXT, TCS_MODE_TEXT, AISHO_MODES,
_internal: { jdn, fromJdn, weekday, shusei, jusei, zokan, zokanStages, findSetsu, findNextSetsu, kanshiIndex, kanshiYear },
};
})();
if (typeof module !== 'undefined' && module.exports) module.exports = SANMEI;