feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具
落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
package natal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Aspect is a major aspect between two chart points.
|
||||
type Aspect struct {
|
||||
A string `json:"a"`
|
||||
B string `json:"b"`
|
||||
ATitle string `json:"a_title"`
|
||||
BTitle string `json:"b_title"`
|
||||
Type string `json:"type"` // conjunction|sextile|square|trine|opposition
|
||||
Angle float64 `json:"angle"`
|
||||
Orb float64 `json:"orb"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
var aspectDefs = []struct {
|
||||
Type string
|
||||
Angle float64
|
||||
Orb float64
|
||||
Label string
|
||||
}{
|
||||
{"conjunction", 0, 8, "合相"},
|
||||
{"sextile", 60, 6, "六合"},
|
||||
{"square", 90, 7, "刑相"},
|
||||
{"trine", 120, 7, "拱相"},
|
||||
{"opposition", 180, 8, "对冲"},
|
||||
}
|
||||
|
||||
// majorKeys used for aspect table (外行星可选由前端过滤).
|
||||
var majorKeys = []string{"sun", "moon", "rise", "mercury", "venus", "mars", "jupiter", "saturn"}
|
||||
|
||||
// Aspects computes major aspects among primary bodies.
|
||||
func Aspects(chart Chart) []Aspect {
|
||||
byKey := map[string]Body{}
|
||||
for _, p := range chart.Planets {
|
||||
byKey[p.Key] = p
|
||||
}
|
||||
var bodies []Body
|
||||
for _, k := range majorKeys {
|
||||
if b, ok := byKey[k]; ok {
|
||||
bodies = append(bodies, b)
|
||||
}
|
||||
}
|
||||
out := make([]Aspect, 0)
|
||||
for i := 0; i < len(bodies); i++ {
|
||||
for j := i + 1; j < len(bodies); j++ {
|
||||
a, b := bodies[i], bodies[j]
|
||||
diff := AngleDiff(a.Lon, b.Lon)
|
||||
for _, def := range aspectDefs {
|
||||
orb := math.Abs(diff - def.Angle)
|
||||
if orb <= def.Orb {
|
||||
out = append(out, Aspect{
|
||||
A: a.Key, B: b.Key, ATitle: a.Title, BTitle: b.Title,
|
||||
Type: def.Type, Angle: def.Angle, Orb: round1(orb),
|
||||
Label: fmt.Sprintf("%s%s%s(容许%.1f°)", a.Title, def.Label, b.Title, orb),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].Orb != out[j].Orb {
|
||||
return out[i].Orb < out[j].Orb
|
||||
}
|
||||
return out[i].Label < out[j].Label
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// AspectsAsMaps for JSON embedding.
|
||||
func AspectsAsMaps(list []Aspect) []map[string]any {
|
||||
out := make([]map[string]any, 0, len(list))
|
||||
for _, a := range list {
|
||||
out = append(out, map[string]any{
|
||||
"a": a.A, "b": a.B, "a_title": a.ATitle, "b_title": a.BTitle,
|
||||
"type": a.Type, "angle": a.Angle, "orb": a.Orb, "label": a.Label,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func round1(x float64) float64 {
|
||||
return math.Round(x*10) / 10
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package natal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAspectsDeterministic(t *testing.T) {
|
||||
c, err := Compute(time.Date(1990, 5, 12, 0, 0, 0, 0, time.UTC), strPtr("12:00"), strPtr("北京"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := Aspects(c)
|
||||
b := Aspects(c)
|
||||
if len(a) == 0 {
|
||||
t.Fatal("expected some aspects")
|
||||
}
|
||||
if len(a) != len(b) || a[0].Label != b[0].Label {
|
||||
t.Fatalf("not deterministic: %+v vs %+v", a[0], b[0])
|
||||
}
|
||||
for _, asp := range a {
|
||||
if asp.Orb < 0 || asp.Orb > 8.1 {
|
||||
t.Fatalf("orb out of range: %+v", asp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
@@ -0,0 +1,148 @@
|
||||
package natal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// City is a built-in birth place with WGS84 coordinates.
|
||||
type City struct {
|
||||
Name string
|
||||
Lat float64
|
||||
Lng float64
|
||||
}
|
||||
|
||||
// Cities lists common CN cities for MVP geocode (no network).
|
||||
var Cities = []City{
|
||||
{"北京", 39.9042, 116.4074},
|
||||
{"上海", 31.2304, 121.4737},
|
||||
{"广州", 23.1291, 113.2644},
|
||||
{"深圳", 22.5431, 114.0579},
|
||||
{"杭州", 30.2741, 120.1551},
|
||||
{"成都", 30.5728, 104.0668},
|
||||
{"重庆", 29.5630, 106.5516},
|
||||
{"武汉", 30.5928, 114.3055},
|
||||
{"西安", 34.3416, 108.9398},
|
||||
{"南京", 32.0603, 118.7969},
|
||||
{"天津", 39.3434, 117.3616},
|
||||
{"苏州", 31.2989, 120.5853},
|
||||
{"长沙", 28.2282, 112.9388},
|
||||
{"郑州", 34.7466, 113.6254},
|
||||
{"青岛", 36.0671, 120.3826},
|
||||
{"大连", 38.9140, 121.6147},
|
||||
{"厦门", 24.4798, 118.0894},
|
||||
{"福州", 26.0745, 119.2965},
|
||||
{"昆明", 25.0389, 102.7183},
|
||||
{"贵阳", 26.6470, 106.6302},
|
||||
{"南宁", 22.8170, 108.3665},
|
||||
{"海口", 20.0440, 110.1999},
|
||||
{"哈尔滨", 45.8038, 126.5349},
|
||||
{"长春", 43.8171, 125.3235},
|
||||
{"沈阳", 41.8057, 123.4315},
|
||||
{"石家庄", 38.0428, 114.5149},
|
||||
{"太原", 37.8706, 112.5489},
|
||||
{"济南", 36.6512, 117.1201},
|
||||
{"合肥", 31.8206, 117.2272},
|
||||
{"南昌", 28.6820, 115.8579},
|
||||
{"兰州", 36.0611, 103.8343},
|
||||
{"乌鲁木齐", 43.8256, 87.6168},
|
||||
{"拉萨", 29.6520, 91.1721},
|
||||
{"呼和浩特", 40.8414, 111.7519},
|
||||
{"银川", 38.4872, 106.2309},
|
||||
{"西宁", 36.6171, 101.7782},
|
||||
{"香港", 22.3193, 114.1694},
|
||||
{"澳门", 22.1987, 113.5439},
|
||||
{"台北", 25.0330, 121.5654},
|
||||
{"宁波", 29.8683, 121.5440},
|
||||
{"无锡", 31.4912, 120.3119},
|
||||
{"佛山", 23.0215, 113.1214},
|
||||
{"东莞", 23.0207, 113.7518},
|
||||
{"温州", 27.9943, 120.6994},
|
||||
{"泉州", 24.8741, 118.6759},
|
||||
{"珠海", 22.2710, 113.5767},
|
||||
}
|
||||
|
||||
// DefaultCoords is used when place is missing (东八区中部近似).
|
||||
const DefaultLat = 30.0
|
||||
const DefaultLng = 114.0
|
||||
|
||||
// ResolvePlace returns lat/lng and whether a named city matched.
|
||||
// Accepts plain city ("杭州") or 省市区 ("浙江省 杭州市 西湖区" / "北京市 市辖区 朝阳区").
|
||||
func ResolvePlace(place string) (lat, lng float64, matched string, ok bool) {
|
||||
place = strings.TrimSpace(place)
|
||||
if place == "" {
|
||||
return DefaultLat, DefaultLng, "", false
|
||||
}
|
||||
// exact city table hit
|
||||
for _, c := range Cities {
|
||||
if c.Name == place {
|
||||
return c.Lat, c.Lng, c.Name, true
|
||||
}
|
||||
}
|
||||
tokens := splitPlace(place)
|
||||
// prefer matching city-level tokens (usually 2nd), then others
|
||||
order := append([]string{}, tokens...)
|
||||
if len(tokens) >= 2 {
|
||||
order = append([]string{tokens[1]}, tokens[0])
|
||||
if len(tokens) >= 3 {
|
||||
order = append(order, tokens[2:]...)
|
||||
}
|
||||
}
|
||||
for _, tok := range order {
|
||||
if tok == "" || tok == "市辖区" || tok == "县" {
|
||||
continue
|
||||
}
|
||||
key := normalizeAdmin(tok)
|
||||
for _, c := range Cities {
|
||||
if c.Name == key || strings.Contains(tok, c.Name) || strings.Contains(c.Name, key) {
|
||||
return c.Lat, c.Lng, c.Name, true
|
||||
}
|
||||
}
|
||||
}
|
||||
// province-only fallback: use first token city if 直辖市
|
||||
if len(tokens) > 0 {
|
||||
key := normalizeAdmin(tokens[0])
|
||||
for _, c := range Cities {
|
||||
if c.Name == key {
|
||||
return c.Lat, c.Lng, c.Name, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return DefaultLat, DefaultLng, "", false
|
||||
}
|
||||
|
||||
func splitPlace(s string) []string {
|
||||
s = strings.ReplaceAll(s, "/", " ")
|
||||
s = strings.ReplaceAll(s, "/", " ")
|
||||
s = strings.ReplaceAll(s, ",", " ")
|
||||
s = strings.ReplaceAll(s, ",", " ")
|
||||
parts := strings.Fields(s)
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeAdmin(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
suffixes := []string{"特别行政区", "维吾尔自治区", "壮族自治区", "回族自治区", "自治区", "省", "市", "地区", "盟"}
|
||||
for _, suf := range suffixes {
|
||||
if strings.HasSuffix(s, suf) && utf8.RuneCountInString(s) > utf8.RuneCountInString(suf) {
|
||||
return strings.TrimSuffix(s, suf)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// CityNames returns names for API/UI pickers.
|
||||
func CityNames() []string {
|
||||
out := make([]string, len(Cities))
|
||||
for i, c := range Cities {
|
||||
out[i] = c.Name
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Package natal computes tropical whole-sign natal charts via Swiss Ephemeris.
|
||||
package natal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/star/ephemeris"
|
||||
)
|
||||
|
||||
// Body is a chart point.
|
||||
type Body struct {
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Lon float64 `json:"lon"`
|
||||
SignKey string `json:"sign_key"`
|
||||
Sign string `json:"sign"`
|
||||
Degree float64 `json:"degree"` // 0–30 within sign
|
||||
House int `json:"house"`
|
||||
Element string `json:"element"`
|
||||
Modality string `json:"modality"`
|
||||
}
|
||||
|
||||
// House is a whole-sign house.
|
||||
type House struct {
|
||||
Num int `json:"num"`
|
||||
Sign string `json:"sign"`
|
||||
Key string `json:"sign_key"`
|
||||
}
|
||||
|
||||
// Chart is a natal chart snapshot.
|
||||
type Chart struct {
|
||||
Sun, Moon, Rise Body
|
||||
Planets []Body
|
||||
Houses []House
|
||||
Lat, Lng float64
|
||||
PlaceLabel string
|
||||
HasTime bool
|
||||
HasPlace bool
|
||||
Note string
|
||||
// Instant is the UTC moment used for ephemeris (noon default when time missing).
|
||||
Instant time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type signMeta struct {
|
||||
Key, Label, Element, Modality string
|
||||
}
|
||||
|
||||
var signs = []signMeta{
|
||||
{"aries", "白羊", "火", "开创"},
|
||||
{"taurus", "金牛", "土", "固定"},
|
||||
{"gemini", "双子", "风", "变动"},
|
||||
{"cancer", "巨蟹", "水", "开创"},
|
||||
{"leo", "狮子", "火", "固定"},
|
||||
{"virgo", "处女", "土", "变动"},
|
||||
{"libra", "天秤", "风", "开创"},
|
||||
{"scorpio", "天蝎", "水", "固定"},
|
||||
{"sagittarius", "射手", "火", "变动"},
|
||||
{"capricorn", "摩羯", "土", "开创"},
|
||||
{"aquarius", "水瓶", "风", "固定"},
|
||||
{"pisces", "双鱼", "水", "变动"},
|
||||
}
|
||||
|
||||
var bodyTitles = map[string]string{
|
||||
"sun": "太阳", "moon": "月亮", "rise": "上升",
|
||||
"mercury": "水星", "venus": "金星", "mars": "火星",
|
||||
"jupiter": "木星", "saturn": "土星", "uranus": "天王星",
|
||||
"neptune": "海王星", "pluto": "冥王星",
|
||||
}
|
||||
|
||||
// Compute builds a natal chart. birthTime is HH:MM local; place is city name.
|
||||
func Compute(birthDate time.Time, birthTime *string, place *string) (Chart, error) {
|
||||
lat, lng := DefaultLat, DefaultLng
|
||||
placeLabel := "默认(未填出生地)"
|
||||
hasPlace := false
|
||||
if place != nil && *place != "" {
|
||||
if la, ln, name, ok := ResolvePlace(*place); ok {
|
||||
lat, lng, placeLabel, hasPlace = la, ln, name, true
|
||||
} else {
|
||||
placeLabel = *place + "(未匹配城市,用默认坐标)"
|
||||
}
|
||||
}
|
||||
|
||||
h, mi := 12, 0 // noon default when time missing
|
||||
hasTime := false
|
||||
if birthTime != nil && *birthTime != "" {
|
||||
if hh, mm, ok := parseHM(*birthTime); ok {
|
||||
h, mi, hasTime = hh, mm, true
|
||||
}
|
||||
}
|
||||
|
||||
// Treat civil time as UTC+8 for CN MVP (deterministic).
|
||||
loc := time.FixedZone("CST", 8*3600)
|
||||
local := time.Date(birthDate.Year(), birthDate.Month(), birthDate.Day(), h, mi, 0, 0, loc)
|
||||
utc := local.UTC()
|
||||
|
||||
return ComputeAt(utc, lat, lng, placeLabel, hasTime, hasPlace)
|
||||
}
|
||||
|
||||
// ComputeAt builds a chart for an exact UTC instant and coordinates.
|
||||
func ComputeAt(utc time.Time, lat, lng float64, placeLabel string, hasTime, hasPlace bool) (Chart, error) {
|
||||
utc = utc.UTC()
|
||||
jd := ephemeris.JulianDayUT(utc)
|
||||
lons, err := ephemeris.AllPlanetLons(jd)
|
||||
if err != nil {
|
||||
return Chart{}, fmt.Errorf("ephemeris: %w", err)
|
||||
}
|
||||
asc, err := ephemeris.Ascendant(jd, lat, lng)
|
||||
if err != nil {
|
||||
return Chart{}, fmt.Errorf("ephemeris asc: %w", err)
|
||||
}
|
||||
|
||||
ch := ChartFromLons(lons, asc, lat, lng, placeLabel, hasTime, hasPlace)
|
||||
ch.Instant = utc
|
||||
ch.Note = fmt.Sprintf("热带黄道 · 整宫制 · 星历 %s。", ephemeris.Backend())
|
||||
if !hasTime {
|
||||
ch.Note += " 未填出生时,上升与宫位按正午估算。"
|
||||
}
|
||||
if !hasPlace {
|
||||
ch.Note += " 填写出生地可提升上升准确度。"
|
||||
}
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// ChartFromLons builds a whole-sign chart from body longitudes and ASC.
|
||||
func ChartFromLons(lons map[string]float64, asc float64, lat, lng float64, placeLabel string, hasTime, hasPlace bool) Chart {
|
||||
ascSignIdx := signIndex(asc)
|
||||
mk := func(key string, lon float64) Body {
|
||||
title := bodyTitles[key]
|
||||
if title == "" {
|
||||
title = key
|
||||
}
|
||||
idx := signIndex(lon)
|
||||
s := signs[idx]
|
||||
house := wholeSignHouse(ascSignIdx, idx)
|
||||
return Body{
|
||||
Key: key, Title: title, Lon: norm360(lon),
|
||||
SignKey: s.Key, Sign: s.Label, Degree: math.Mod(norm360(lon), 30),
|
||||
House: house, Element: s.Element, Modality: s.Modality,
|
||||
}
|
||||
}
|
||||
|
||||
sun := mk("sun", lons["sun"])
|
||||
moon := mk("moon", lons["moon"])
|
||||
rise := mk("rise", asc)
|
||||
|
||||
planets := []Body{
|
||||
sun, moon, rise,
|
||||
mk("mercury", lons["mercury"]),
|
||||
mk("venus", lons["venus"]),
|
||||
mk("mars", lons["mars"]),
|
||||
mk("jupiter", lons["jupiter"]),
|
||||
mk("saturn", lons["saturn"]),
|
||||
mk("uranus", lons["uranus"]),
|
||||
mk("neptune", lons["neptune"]),
|
||||
mk("pluto", lons["pluto"]),
|
||||
}
|
||||
|
||||
houses := make([]House, 12)
|
||||
for i := 0; i < 12; i++ {
|
||||
idx := (ascSignIdx + i) % 12
|
||||
houses[i] = House{Num: i + 1, Sign: signs[idx].Label, Key: signs[idx].Key}
|
||||
}
|
||||
|
||||
return Chart{
|
||||
Sun: sun, Moon: moon, Rise: rise, Planets: planets, Houses: houses,
|
||||
Lat: lat, Lng: lng, PlaceLabel: placeLabel, HasTime: hasTime, HasPlace: hasPlace,
|
||||
}
|
||||
}
|
||||
|
||||
// BodyLon returns longitude for a key, or 0.
|
||||
func BodyLon(c Chart, key string) float64 {
|
||||
for _, p := range c.Planets {
|
||||
if p.Key == key {
|
||||
return p.Lon
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// SignMetaByKey returns label/element for a sign key.
|
||||
func SignMetaByKey(key string) (label, element, modality string) {
|
||||
for _, s := range signs {
|
||||
if s.Key == key {
|
||||
return s.Label, s.Element, s.Modality
|
||||
}
|
||||
}
|
||||
return key, "", ""
|
||||
}
|
||||
|
||||
// SignByIndex returns sign meta.
|
||||
func SignByIndex(i int) (key, label, element, modality string) {
|
||||
s := signs[((i%12)+12)%12]
|
||||
return s.Key, s.Label, s.Element, s.Modality
|
||||
}
|
||||
|
||||
func wholeSignHouse(ascIdx, bodyIdx int) int {
|
||||
return ((bodyIdx-ascIdx)%12+12)%12 + 1
|
||||
}
|
||||
|
||||
func signIndex(lon float64) int {
|
||||
return int(math.Floor(norm360(lon)/30.0)) % 12
|
||||
}
|
||||
|
||||
func parseHM(s string) (h, m int, ok bool) {
|
||||
var hh, mm int
|
||||
n, err := fmt.Sscanf(s, "%d:%d", &hh, &mm)
|
||||
if err != nil || n < 1 || hh < 0 || hh > 23 {
|
||||
return 0, 0, false
|
||||
}
|
||||
if n == 1 {
|
||||
mm = 0
|
||||
}
|
||||
if mm < 0 || mm > 59 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return hh, mm, true
|
||||
}
|
||||
|
||||
func norm360(x float64) float64 {
|
||||
x = math.Mod(x, 360)
|
||||
if x < 0 {
|
||||
x += 360
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// AngleDiff returns smallest absolute ecliptic separation.
|
||||
func AngleDiff(a, b float64) float64 {
|
||||
d := math.Abs(norm360(a) - norm360(b))
|
||||
if d > 180 {
|
||||
d = 360 - d
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// MidpointLon returns the shorter-arc midpoint of two longitudes.
|
||||
func MidpointLon(a, b float64) float64 {
|
||||
a, b = norm360(a), norm360(b)
|
||||
d := b - a
|
||||
if d > 180 {
|
||||
d -= 360
|
||||
} else if d < -180 {
|
||||
d += 360
|
||||
}
|
||||
return norm360(a + d/2)
|
||||
}
|
||||
|
||||
// SignIndex is exported for overlay house mapping.
|
||||
func SignIndex(lon float64) int { return signIndex(lon) }
|
||||
|
||||
// WholeSignHouse exported for overlay.
|
||||
func WholeSignHouse(ascIdx, bodyIdx int) int { return wholeSignHouse(ascIdx, bodyIdx) }
|
||||
@@ -0,0 +1,55 @@
|
||||
package natal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestComputeDeterministic(t *testing.T) {
|
||||
birth := time.Date(1990, 5, 12, 0, 0, 0, 0, time.UTC)
|
||||
bt := "10:30"
|
||||
place := "上海"
|
||||
a, err := Compute(birth, &bt, &place)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := Compute(birth, &bt, &place)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if a.Sun.Sign != b.Sun.Sign || a.Moon.Lon != b.Moon.Lon || a.Rise.Sign != b.Rise.Sign {
|
||||
t.Fatal("not deterministic")
|
||||
}
|
||||
if len(a.Planets) < 8 || len(a.Houses) != 12 {
|
||||
t.Fatalf("planets=%d houses=%d", len(a.Planets), len(a.Houses))
|
||||
}
|
||||
if a.Sun.Sign == "" {
|
||||
t.Fatal("empty sun")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePlaceBeijing(t *testing.T) {
|
||||
lat, lng, name, ok := ResolvePlace("北京")
|
||||
if !ok || name != "北京" || lat < 39 || lng < 116 {
|
||||
t.Fatalf("got %v %v %s %v", lat, lng, name, ok)
|
||||
}
|
||||
_, _, name2, ok2 := ResolvePlace("北京市 市辖区 朝阳区")
|
||||
if !ok2 || name2 != "北京" {
|
||||
t.Fatalf("pca resolve got %s %v", name2, ok2)
|
||||
}
|
||||
_, _, name3, ok3 := ResolvePlace("浙江省 杭州市 西湖区")
|
||||
if !ok3 || name3 != "杭州" {
|
||||
t.Fatalf("hangzhou resolve got %s %v", name3, ok3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSunSignMay(t *testing.T) {
|
||||
c, err := Compute(time.Date(1990, 5, 12, 0, 0, 0, 0, time.UTC), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// mid-May → Taurus
|
||||
if c.Sun.SignKey != "taurus" {
|
||||
t.Fatalf("want taurus got %s", c.Sun.SignKey)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user