-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathevaluate.go
More file actions
1454 lines (1304 loc) · 49.7 KB
/
evaluate.go
File metadata and controls
1454 lines (1304 loc) · 49.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package ditzy
import (
"fmt"
"math"
"sort"
"time"
)
// allUTCOffsets contains every UTC offset used worldwide.
// Integer offsets: -12 to +14
// Half-hour offsets: +5:30 (India), +9:30 (Australia Central), etc.
// 45-minute offsets: +5:45 (Nepal), +12:45 (Chatham Islands).
var allUTCOffsets = []float64{
-12, -11, -10, -9.5, -9, -8, -7, -6, -5, -4, -3.5, -3, -2, -1,
0, 1, 2, 3, 3.5, 4, 4.5, 5, 5.5, 5.75, 6, 6.5, 7, 8, 8.75, 9, 9.5, 10, 10.5, 11, 12, 12.75, 13, 14,
}
// devPopulationWeight represents relative software developer population by UTC offset.
// Applies to GitHub, Gitee, and other code hosting platforms.
// Higher values = larger developer population = more likely timezone.
// Scale: 0.1 (tiny) to 10.0 (massive).
var devPopulationWeight = map[float64]float64{
// Americas
-12: 0.1, // Baker Island (uninhabited)
-11: 0.2, // American Samoa, Niue
-10: 0.5, // Hawaii, Tahiti
-9.5: 0.1, // Marquesas Islands
-9: 0.3, // Alaska
-8: 9.0, // US/Canada Pacific (Silicon Valley, Seattle, Vancouver)
-7: 4.0, // US/Canada Mountain (Denver, Phoenix, Calgary)
-6: 4.0, // US/Canada Central, Mexico City
-5: 7.0, // US/Canada Eastern (NYC, Boston, Toronto), Colombia, Peru
-4: 2.5, // Atlantic Canada, Venezuela, Bolivia, Chile, Brazil (parts)
-3.5: 0.2, // Newfoundland
-3: 4.0, // Brazil (São Paulo, Rio), Argentina, Uruguay
-2: 0.1, // Mid-Atlantic (South Georgia)
-1: 0.2, // Azores, Cape Verde
// Europe & Africa
0: 5.0, // UK, Ireland, Portugal, Iceland
1: 7.0, // Central Europe (Germany, France, Spain, Italy, Netherlands, Poland)
2: 5.0, // Eastern Europe (Finland, Greece, Romania, Ukraine, Israel, South Africa)
3: 3.0, // Moscow, Turkey, Saudi Arabia, Kenya
3.5: 0.5, // Iran
// Asia & Oceania
4: 1.0, // UAE, Azerbaijan, Georgia
4.5: 1.0, // Afghanistan (boosted from 0.2)
5: 1.5, // Pakistan, Uzbekistan
5.5: 9.0, // India, Sri Lanka (massive developer population)
5.75: 1.0, // Nepal (boosted from 0.2)
6: 1.0, // Bangladesh, Kazakhstan
6.5: 0.2, // Myanmar
7: 3.0, // Vietnam, Thailand, Indonesia (parts)
8: 10.0, // China (GitHub+Gitee), Singapore, Hong Kong, Taiwan, Malaysia, Philippines
8.75: 0.1, // Australia (Eucla - tiny)
9: 6.0, // Japan, South Korea
9.5: 0.4, // Australia Central (Adelaide, Darwin)
10: 2.0, // Australia Eastern (Sydney, Melbourne), Papua New Guinea
10.5: 0.1, // Lord Howe Island
11: 0.2, // Solomon Islands, Vanuatu
12: 1.0, // New Zealand, Fiji
12.75: 0.05, // Chatham Islands (~600 people total)
13: 0.1, // Tonga, Samoa
14: 0.05, // Line Islands
}
// formatUTCOffset formats a float offset as a timezone string (e.g., "UTC+5:30").
func formatUTCOffset(offset float64) string {
hours := int(offset)
frac := offset - float64(hours)
if frac == 0 {
return fmt.Sprintf("UTC%+d", hours)
}
minutes := int(math.Abs(frac) * 60)
if offset >= 0 {
return fmt.Sprintf("UTC+%d:%02d", hours, minutes)
}
return fmt.Sprintf("UTC%d:%02d", hours, minutes)
}
// localToUTC converts a local hour to UTC hour given an offset.
func localToUTC(localHour int, offset float64) int {
return int(math.Mod(float64(localHour)-offset+24, 24))
}
// utcToLocal converts a UTC hour to local hour given an offset.
func utcToLocal(utcHour int, offset float64) int {
return int(math.Mod(float64(utcHour)+offset+24, 24))
}
// globalLunchPattern represents the best lunch pattern found globally in UTC.
type globalLunchPattern struct {
startUTC float64
endUTC float64
confidence float64
dropPercent float64
}
// evaluationInput contains all parameters needed for timezone candidate evaluation.
type evaluationInput struct {
newestActivity time.Time
hourCounts map[int]int
halfHourCounts map[float64]int
username string
profileTimezone string
quietHours []int
bestGlobalLunch globalLunchPattern
midQuiet float64
activeStart float64
totalActivity int
}
// evaluationCandidate represents a timezone detection result with evidence.
type evaluationCandidate struct {
timezone string
scoringDetails []string
sleepBucketsUTC []float64
sleepMidUTC float64
peakStartUTC float64
offset float64
lunchConfidence float64
lunchEndUTC float64
workStartUTC float64
workEndUTC float64
lunchStartUTC float64
lunchDipStrength float64
confidence float64
peakEndUTC float64
eveningActivity int
peakCount int
peakTimeReasonable bool
sleepReasonable bool
workHoursReasonable bool
lunchReasonable bool
isProfile bool
}
// calculateEasternLunchBonus calculates bonus points for Eastern timezone lunch patterns.
func calculateEasternLunchBonus(dropRatio, lunchLocalStart float64) (bonus float64, adjustment string) {
if dropRatio > 0.7 && lunchLocalStart >= 11.0 && lunchLocalStart <= 12.0 {
return 8.0, fmt.Sprintf("+8 (Eastern 11:30am lunch %.1f%% drop)", dropRatio*100)
}
if dropRatio > 0.5 && lunchLocalStart >= 11.0 && lunchLocalStart <= 12.5 {
return 5.0, fmt.Sprintf("+5 (Eastern lunch %.1f%% drop)", dropRatio*100)
}
return 0, ""
}
// findBestWorkStartForTimezone finds the most likely work start time for a given timezone.
func findBestWorkStartForTimezone(halfHourCounts map[float64]int, testOffset float64, fallbackStart float64) float64 {
type period struct {
startUTC float64
activity int
}
var periods []period
for hour := 0.0; hour < 24.0; hour += 0.5 {
count := halfHourCounts[hour]
nextCount := halfHourCounts[math.Mod(hour+0.5, 24)]
if count >= 3 && nextCount >= 3 {
periods = append(periods, period{
startUTC: hour,
activity: count + nextCount,
})
}
}
if len(periods) == 0 {
return fallbackStart
}
bestScore := -1000.0
bestStart := fallbackStart
for _, p := range periods {
localStart := math.Mod(p.startUTC+testOffset+24, 24)
score := 0.0
switch {
case localStart >= 7 && localStart <= 11:
score += 100
if localStart >= 8 && localStart <= 10 {
score += 50
}
case localStart >= 6 && localStart < 7:
score += 50
case localStart >= 11 && localStart <= 14:
score += 20
case localStart >= 14 && localStart <= 18:
score -= 50
default:
score -= 100
}
score += float64(p.activity)
if score > bestScore {
bestScore = score
bestStart = p.startUTC
}
}
return bestStart
}
// evaluate evaluates multiple timezone offsets to find the best candidates.
// This is the core timezone detection algorithm with all scoring nuances.
//
//nolint:gocognit,maintidx,revive // timezone detection requires many interconnected heuristics
func evaluate(input evaluationInput) []evaluationCandidate {
var candidates []evaluationCandidate
// Pre-calculate peak activity hour
maxActivity := 0
maxActivityHour := -1
for hour, count := range input.hourCounts {
if count > maxActivity {
maxActivity = count
maxActivityHour = hour
}
}
// evaluate all possible UTC offsets including fractional ones (e.g., +5:30 for India)
for _, testOffset := range allUTCOffsets {
// Lunch timing analysis
testLunchStart, testLunchEnd, testLunchConf := detectLunchBreak(input.halfHourCounts, testOffset)
lunchLocalStart := math.Mod(testLunchStart+testOffset+24, 24)
// Find the best work period for this specific timezone
testActiveStartUTC := findBestWorkStartForTimezone(input.halfHourCounts, testOffset, input.activeStart)
testWorkStart := math.Mod(testActiveStartUTC+testOffset+24, 24)
firstActivityLocal := testWorkStart
// Calculate work END time
testActiveEndUTC := 0.0
for hour := 23.5; hour >= 0; hour -= 0.5 {
if input.halfHourCounts[hour] > 0 {
testActiveEndUTC = hour
break
}
}
// Lunch is reasonable if detected in 10am-2:30pm window with good confidence
lunchReasonable := testLunchStart >= 0 &&
lunchLocalStart >= 10.0 &&
lunchLocalStart <= 14.5 &&
testLunchConf >= 0.3 &&
lunchLocalStart >= firstActivityLocal+1.0
// Calculate lunch dip strength
lunchDipStrength := 0.0
if testLunchConf > 0 && testLunchStart >= 0 {
beforeLunchBucket := testLunchStart - 1.0
if beforeLunchBucket < 0 {
beforeLunchBucket += 24
}
beforeActivity := float64(input.halfHourCounts[beforeLunchBucket])
lunchActivity := float64(input.halfHourCounts[testLunchStart])
if beforeActivity > 0 {
lunchDipStrength = (beforeActivity - lunchActivity) / beforeActivity
}
}
// Detect sleep periods for this specific timezone offset
sleepBucketsUTC := detectSleepBuckets(input.halfHourCounts, testOffset)
// Calculate peak productivity using 30-minute buckets
peakStartUTC := 0.0
peakEndUTC := 0.0
peakCount := 0
peakBucket := -1.0
peakActivity := 0
for bucket, count := range input.halfHourCounts {
if count > peakActivity {
peakActivity = count
peakBucket = bucket
}
}
if peakBucket >= 0 {
peakStartUTC = peakBucket
peakEndUTC = peakBucket + 0.5 // 30-minute window
peakCount = peakActivity
}
// Sleep timing analysis - calculate sleep midpoint
var sleepMidUTC float64
if len(sleepBucketsUTC) > 0 {
hasEarlyMorning := false
hasEvening := false
for _, bucket := range sleepBucketsUTC {
if bucket >= 0 && bucket < 8 {
hasEarlyMorning = true
}
if bucket >= 18 {
hasEvening = true
}
}
if hasEarlyMorning && hasEvening {
sleepSum := 0.0
for _, bucket := range sleepBucketsUTC {
if bucket >= 12 {
sleepSum += bucket
} else {
sleepSum += bucket + 24
}
}
sleepMidUTC = sleepSum / float64(len(sleepBucketsUTC))
sleepMidUTC = math.Mod(sleepMidUTC, 24)
} else {
sleepSum := 0.0
for _, bucket := range sleepBucketsUTC {
sleepSum += bucket
}
sleepMidUTC = sleepSum / float64(len(sleepBucketsUTC))
}
} else {
sleepMidUTC = input.midQuiet
}
sleepLocalMid := math.Mod(sleepMidUTC+testOffset+24, 24)
sleepReasonable := (sleepLocalMid >= 0 && sleepLocalMid <= 10) || sleepLocalMid >= 22
// Work hours analysis
workReasonable := firstActivityLocal >= 6 && firstActivityLocal <= 11
// Evening activity (7-11pm local)
eveningActivity := 0
for localHour := 19; localHour <= 23; localHour++ {
utcHour := int(math.Mod(float64(localHour)-testOffset+24, 24))
eveningActivity += input.hourCounts[utcHour]
}
// European timezone validation
europeanMorningActivityCheck := true
if testOffset >= 0 && testOffset <= 3 {
morningActivity := 0
for localHour := 8; localHour <= 10; localHour++ {
utcHour := int(math.Mod(float64(localHour)-testOffset+24, 24))
morningActivity += input.hourCounts[utcHour]
}
if morningActivity == 0 {
europeanMorningActivityCheck = false
}
}
// Calculate overall confidence score
testConfidence := 0.0
adjustments := []string{}
// Sleep timing scoring (15 points max)
if sleepReasonable {
sleepStartUTC := -1
if len(input.quietHours) > 0 {
sleepStartUTC = input.quietHours[0]
}
switch {
case sleepLocalMid >= 1 && sleepLocalMid <= 4:
testConfidence += 12
adjustments = append(adjustments, fmt.Sprintf("+12 (perfect sleep 1-4am, mid=%.1f)", sleepLocalMid))
if sleepStartUTC >= 0 {
sleepStartLocal := math.Mod(float64(sleepStartUTC)+testOffset+24, 24)
if sleepStartLocal >= 21 && sleepStartLocal <= 23 {
testConfidence += 3
adjustments = append(adjustments, fmt.Sprintf("+3 (early sleep bonus, start=%.0fpm)", sleepStartLocal-12))
}
}
case sleepLocalMid >= 0 && sleepLocalMid <= 5:
testConfidence += 10
adjustments = append(adjustments, fmt.Sprintf("+10 (good sleep, mid=%.1f)", sleepLocalMid))
if sleepStartUTC >= 0 {
sleepStartLocal := math.Mod(float64(sleepStartUTC)+testOffset+24, 24)
if sleepStartLocal >= 21 && sleepStartLocal <= 23 {
testConfidence += 2
adjustments = append(adjustments, fmt.Sprintf("+2 (early sleep, start=%.0fpm)", sleepStartLocal-12))
}
}
case sleepLocalMid >= 22 || sleepLocalMid <= 10:
testConfidence += 5
adjustments = append(adjustments, fmt.Sprintf("+5 (acceptable sleep, mid=%.1f)", sleepLocalMid))
default:
if sleepLocalMid >= 10 && sleepLocalMid <= 22 {
testConfidence -= 100
adjustments = append(adjustments, fmt.Sprintf("-100 (IMPOSSIBLE daytime sleep, mid=%.1f)", sleepLocalMid))
} else {
testConfidence -= 20
adjustments = append(adjustments, fmt.Sprintf("-20 (very unusual sleep, mid=%.1f)", sleepLocalMid))
}
}
} else {
adjustments = append(adjustments, "0 (no reasonable sleep pattern)")
}
// Lunch timing scoring (15 points max)
if lunchReasonable {
var lunchScore float64
switch {
case lunchLocalStart >= 11.75 && lunchLocalStart <= 12.25:
lunchScore = 15
if input.bestGlobalLunch.confidence > 0 {
globalLunchLocalTime := math.Mod(input.bestGlobalLunch.startUTC+testOffset+24, 24)
if math.Abs(globalLunchLocalTime-12.0) < 0.5 {
lunchScore += 5
adjustments = append(adjustments, fmt.Sprintf("+%.0f (perfect noon lunch matching global pattern)", lunchScore))
} else {
adjustments = append(adjustments, fmt.Sprintf("+%.0f (perfect noon lunch)", lunchScore))
}
} else {
adjustments = append(adjustments, fmt.Sprintf("+%.0f (perfect noon lunch)", lunchScore))
}
case lunchLocalStart >= 11.5 && lunchLocalStart <= 13.5:
lunchScore = 10
adjustments = append(adjustments, "+10 (good lunch timing 11:30am-1:30pm)")
case lunchLocalStart >= 11.0 && lunchLocalStart <= 14.0:
lunchScore = 8
adjustments = append(adjustments, "+8 (acceptable lunch timing 11am-2pm)")
case lunchLocalStart >= 10.5 && lunchLocalStart <= 14.5:
lunchScore = 6
adjustments = append(adjustments, "+6 (acceptable lunch timing 10:30am-2:30pm)")
default:
lunchScore = 2
adjustments = append(adjustments, fmt.Sprintf("+2 (unusual lunch timing at %.1f)", lunchLocalStart))
}
// Dip strength bonus
dipBonus := 0.0
switch {
case lunchDipStrength >= 0.8:
dipBonus = 5.0
case lunchDipStrength >= 0.6:
dipBonus = 3.0
case lunchDipStrength >= 0.4:
dipBonus = 1.5
}
if dipBonus > 0 {
adjustments = append(adjustments, fmt.Sprintf("+%.1f (lunch dip strength %.1f%%)", dipBonus, lunchDipStrength*100))
}
lunchScore += dipBonus
// Penalty for weak late lunches
if lunchLocalStart > 13.5 && lunchDipStrength < 0.4 {
oldScore := lunchScore
lunchScore *= 0.3
adjustments = append(adjustments, fmt.Sprintf("-%.1f (weak late lunch penalty)", oldScore-lunchScore))
}
// Penalty for lunch at end of work day
activeEndLocal := 0.0
for hour := 23; hour >= 0; hour-- {
utcHour := localToUTC(hour, testOffset)
if input.hourCounts[utcHour] > 0 {
activeEndLocal = float64(hour)
break
}
}
if activeEndLocal > 0 && lunchLocalStart >= activeEndLocal-1.5 {
lunchScore -= 10
adjustments = append(adjustments, fmt.Sprintf("-10 (lunch at end of work day %.1f vs work end %.0f)", lunchLocalStart, activeEndLocal))
}
finalLunchScore := math.Min(15, lunchScore)
testConfidence += finalLunchScore
} else if testLunchStart < 0 {
testConfidence -= 5
adjustments = append(adjustments, "-5 (no lunch detected)")
}
// Work hours scoring with penalties for early starts
if firstActivityLocal < 24 {
preciseWorkStart := firstActivityLocal
afternoonProductivity := 0
for localHour := 13; localHour <= 16; localHour++ {
utcHour := localToUTC(localHour, testOffset)
afternoonProductivity += input.hourCounts[utcHour]
}
switch {
case preciseWorkStart <= 4.0:
testConfidence -= 50
adjustments = append(adjustments, fmt.Sprintf("-50 (impossible %.1fam work start)", preciseWorkStart))
if preciseWorkStart < 2.0 {
testConfidence -= 30
adjustments = append(adjustments, fmt.Sprintf("-30 (extra penalty for midnight-%.1fam)", preciseWorkStart))
}
case preciseWorkStart >= 5.0 && preciseWorkStart < 5.5:
penalty := -10.0
if afternoonProductivity > 40 {
penalty = -5.0
adjustments = append(adjustments, "-5 (early 5:00am start but good afternoon productivity)")
} else {
adjustments = append(adjustments, "-10 (early 5:00am start)")
}
testConfidence += penalty
case preciseWorkStart >= 5.5 && preciseWorkStart < 6.0:
penalty := -5.0
if afternoonProductivity > 40 {
penalty = -2.0
adjustments = append(adjustments, "-2 (5:30am start with good afternoon productivity)")
} else {
adjustments = append(adjustments, "-5 (5:30am start)")
}
testConfidence += penalty
case workReasonable:
actualWorkStart := int(preciseWorkStart)
switch {
case actualWorkStart >= 8 && actualWorkStart <= 10:
testConfidence += 12
adjustments = append(adjustments, fmt.Sprintf("+12 (good work start %dam)", actualWorkStart))
case actualWorkStart == 7:
testConfidence += 8
adjustments = append(adjustments, "+8 (early bird 7am work start)")
case actualWorkStart == 6:
testConfidence += 4
adjustments = append(adjustments, "+4 (early 6am work start)")
case actualWorkStart == 11:
testConfidence += 4
adjustments = append(adjustments, "+4 (late 11am work start)")
default:
testConfidence++
adjustments = append(adjustments, fmt.Sprintf("+1 (very unusual work start %dam)", actualWorkStart))
}
default:
actualWorkStart := int(preciseWorkStart)
testConfidence -= 5
adjustments = append(adjustments, fmt.Sprintf("-5 (unreasonable work hours %dam)", actualWorkStart))
}
}
// Raw first activity penalty
rawFirstActivityLocal := math.Mod(input.activeStart+testOffset+24, 24)
switch {
case rawFirstActivityLocal < 4.0 && rawFirstActivityLocal > 0.5:
penalty := 30.0 - (rawFirstActivityLocal * 5.0)
testConfidence -= penalty
adjustments = append(adjustments, fmt.Sprintf("-%.0f (raw first activity at %.1fam - very likely wrong timezone)", penalty, rawFirstActivityLocal))
case rawFirstActivityLocal >= 4.0 && rawFirstActivityLocal < 6.0:
penalty := 20.0 - (rawFirstActivityLocal * 3.0)
if penalty > 0 {
testConfidence -= penalty
adjustments = append(adjustments, fmt.Sprintf("-%.0f (raw first activity at %.1fam - likely wrong timezone)", penalty, rawFirstActivityLocal))
}
}
// Business day pattern bonus
businessDayActivity := 0
for localHour := 6; localHour <= 18; localHour++ {
utcHour := localToUTC(localHour, testOffset)
businessDayActivity += input.hourCounts[utcHour]
}
businessDayRatio := float64(businessDayActivity) / float64(input.totalActivity)
if businessDayRatio > 0.75 && workReasonable {
businessBonus := 15.0 * (businessDayRatio - 0.5)
testConfidence += businessBonus
adjustments = append(adjustments, fmt.Sprintf("+%.1f (business day pattern: %.1f%% activity 6am-6pm)", businessBonus, businessDayRatio*100))
}
// Evening activity bonus
eveningRatio := float64(eveningActivity) / float64(input.totalActivity)
lateAfternoonActivity := 0
for localHour := 17; localHour <= 18; localHour++ {
utcHour := localToUTC(localHour, testOffset)
lateAfternoonActivity += input.hourCounts[utcHour]
}
lateAfternoonRatio := float64(lateAfternoonActivity) / float64(input.totalActivity)
//nolint:gocritic // if-else chain more readable than switch for ratio comparisons
if eveningRatio > 0.2 {
eveningPoints := 8.0 + math.Min(7.0, (eveningRatio-0.2)*35.0)
testConfidence += eveningPoints
adjustments = append(adjustments, fmt.Sprintf("+%.1f (evening 7-11pm: %.1f%% - personal timezone signal)", eveningPoints, eveningRatio*100))
if (testOffset == 10 || testOffset == 11) && eveningRatio > 0.25 && input.totalActivity >= 50 {
australiaEvening := 8.0
testConfidence += australiaEvening
adjustments = append(adjustments, fmt.Sprintf("+%.1f (Australia %s with strong evening activity)", australiaEvening, formatUTCOffset(testOffset)))
}
} else if eveningRatio > 0.1 {
eveningPoints := 5.0 * (eveningRatio / 0.1)
testConfidence += eveningPoints
adjustments = append(adjustments, fmt.Sprintf("+%.1f (moderate evening: %.1f%%)", eveningPoints, eveningRatio*100))
} else if eveningActivity >= 3 {
eveningPoints := float64(eveningActivity) * 2.0
if eveningPoints > 10 {
eveningPoints = 10
}
testConfidence += eveningPoints
adjustments = append(adjustments, fmt.Sprintf("+%.1f (evening events: %d - sparse data bonus)", eveningPoints, eveningActivity))
} else if eveningRatio == 0 && testOffset >= 5 && testOffset <= 9 {
testConfidence -= 15
adjustments = append(adjustments, "-15 (ZERO evening activity in Asian timezone - suspicious)")
}
if lateAfternoonRatio > 0.15 && eveningRatio < 0.1 {
testConfidence -= 3
adjustments = append(adjustments, fmt.Sprintf("-3 (high 5-6pm %.1f%% but low evening %.1f%%)", lateAfternoonRatio*100, eveningRatio*100))
}
// Early morning penalties
earlyMorningActivity := 0
for localHour := range 6 {
utcHour := localToUTC(localHour, testOffset)
earlyMorningActivity += input.hourCounts[utcHour]
}
noonToSixActivity := 0
for localHour := 12; localHour <= 17; localHour++ {
utcHour := localToUTC(localHour, testOffset)
noonToSixActivity += input.hourCounts[utcHour]
}
if input.totalActivity > 0 {
earlyMorningRatio := float64(earlyMorningActivity) / float64(input.totalActivity)
afternoonRatio := float64(noonToSixActivity) / float64(input.totalActivity)
switch {
case earlyMorningActivity > noonToSixActivity && earlyMorningRatio > 0.15:
testConfidence -= 50
adjustments = append(adjustments, fmt.Sprintf("-50 (MORE night activity %.1f%% than afternoon %.1f%%)",
earlyMorningRatio*100, afternoonRatio*100))
case earlyMorningRatio > 0.25:
testConfidence -= 30
adjustments = append(adjustments, fmt.Sprintf("-30 (excessive midnight-6am activity: %.1f%%)", earlyMorningRatio*100))
case earlyMorningRatio > 0.15:
testConfidence -= 10
adjustments = append(adjustments, fmt.Sprintf("-10 (high midnight-6am activity: %.1f%%)", earlyMorningRatio*100))
}
}
// Sustained 3-6am activity penalty
deepSleepConsecutive := 0
deepSleepEvents := 0
maxDeepSleepConsecutive := 0
for localHalfHour := 3.0; localHalfHour < 6.0; localHalfHour += 0.5 {
utcHalfHour := localHalfHour - testOffset
for utcHalfHour < 0 {
utcHalfHour += 24
}
for utcHalfHour >= 24 {
utcHalfHour -= 24
}
count := input.halfHourCounts[utcHalfHour]
if count > 0 {
deepSleepConsecutive++
deepSleepEvents += count
if deepSleepConsecutive > maxDeepSleepConsecutive {
maxDeepSleepConsecutive = deepSleepConsecutive
}
} else {
deepSleepConsecutive = 0
}
}
if maxDeepSleepConsecutive >= 3 && deepSleepEvents >= 5 {
penalty := float64(maxDeepSleepConsecutive) * 15.0
if deepSleepEvents >= 10 {
penalty += 15.0
}
if maxDeepSleepConsecutive >= 4 {
penalty += 20.0
}
testConfidence -= penalty
adjustments = append(adjustments, fmt.Sprintf("-%.0f (sustained 3-6am activity: %d consecutive buckets, %d events - wake pattern)",
penalty, maxDeepSleepConsecutive, deepSleepEvents))
}
// Work hours activity bonus
workHoursActivity := 0
for localHour := 9; localHour <= 17; localHour++ {
utcHour := localToUTC(localHour, testOffset)
workHoursActivity += input.hourCounts[utcHour]
}
if workHoursActivity > 0 && input.totalActivity > 0 {
workRatio := float64(workHoursActivity) / float64(input.totalActivity)
workHoursBonus := 2 * math.Min(1.0, workRatio*1.5)
testConfidence += workHoursBonus
if workHoursBonus > 0 {
adjustments = append(adjustments, fmt.Sprintf("+%.1f (work hours activity %.1f%%)", workHoursBonus, workRatio*100))
}
}
// Peak activity timing bonus
peakReasonable := false
if maxActivityHour >= 0 {
peakLocalHour := utcToLocal(maxActivityHour, testOffset)
peakReasonable = (peakLocalHour >= 9 && peakLocalHour <= 16) || (peakLocalHour >= 18 && peakLocalHour <= 21)
var peakBonus float64
switch {
case peakLocalHour >= 17 && peakLocalHour <= 18:
if maxActivity >= 10 {
peakBonus = -8.0
adjustments = append(adjustments, fmt.Sprintf("-8 (suspicious peak at %d:00, dinner/transition time)", peakLocalHour))
} else {
peakBonus = 0.0
adjustments = append(adjustments, fmt.Sprintf("0 (peak at %d:00 with only %d events - sparse data)", peakLocalHour, maxActivity))
}
case peakLocalHour >= 19 && peakLocalHour <= 22:
peakBonus = 2.0
adjustments = append(adjustments, fmt.Sprintf("+2 (evening OSS peak at %dpm)", peakLocalHour-12))
case peakLocalHour == 23:
peakBonus = -5.0
adjustments = append(adjustments, "-5 (late peak at 11pm)")
case peakLocalHour <= 6:
peakBonus = -20.0
adjustments = append(adjustments, fmt.Sprintf("-20 (peak at %dam - night owl unlikely)", peakLocalHour))
case peakLocalHour >= 13 && peakLocalHour <= 15:
peakBonus = 5.0
adjustments = append(adjustments, fmt.Sprintf("+5 (peak at %dpm ideal)", peakLocalHour-12))
case peakLocalHour >= 9 && peakLocalHour <= 12:
peakBonus = 3.0
adjustments = append(adjustments, fmt.Sprintf("+3 (peak at %dam good morning)", peakLocalHour))
case peakLocalHour >= 7 && peakLocalHour <= 8:
peakBonus = -5.0
adjustments = append(adjustments, fmt.Sprintf("-5 (suspicious early peak at %dam)", peakLocalHour))
case peakLocalHour == 16:
peakBonus = 3.0
adjustments = append(adjustments, "+3 (peak at 4pm good work time)")
default:
peakBonus = 0.0
adjustments = append(adjustments, fmt.Sprintf("0 (unusual peak time %d:00)", peakLocalHour))
}
testConfidence += peakBonus
}
// Late work start penalty
if firstActivityLocal >= 14 {
penalty := -20.0
if input.totalActivity < 30 {
penalty = -10.0
}
if eveningRatio > 0.1 {
penalty /= 2
adjustments = append(adjustments, fmt.Sprintf("%.0f (work starts after 2pm at %.1f, reduced due to %.0f%% evening activity)", penalty, firstActivityLocal, eveningRatio*100))
} else {
adjustments = append(adjustments, fmt.Sprintf("%.0f (work starts after 2pm at %.1f)", penalty, firstActivityLocal))
}
testConfidence += penalty
} else if firstActivityLocal >= 12 {
penalty := -10.0
if input.totalActivity < 30 {
penalty = -5.0
}
if eveningRatio > 0.1 {
penalty /= 2
adjustments = append(adjustments, fmt.Sprintf("%.0f (work starts after noon at %.1f, reduced due to %.0f%% evening activity)", penalty, firstActivityLocal, eveningRatio*100))
} else {
adjustments = append(adjustments, fmt.Sprintf("%.0f (work starts after noon at %.1f)", penalty, firstActivityLocal))
}
testConfidence += penalty
}
// Night vs day average penalty
nightActivity := 0
nightHours := 0
dayActivity := 0
dayHours := 0
for hour, count := range input.hourCounts {
localHour := utcToLocal(hour, testOffset)
if localHour >= 0 && localHour < 8 {
nightActivity += count
if count > 0 {
nightHours++
}
} else {
dayActivity += count
if count > 0 {
dayHours++
}
}
}
nightAvg := 0.0
if nightHours > 0 {
nightAvg = float64(nightActivity) / 8.0
}
dayAvg := 0.0
if dayHours > 0 {
dayAvg = float64(dayActivity) / 16.0
}
if nightAvg > dayAvg && nightActivity > 10 {
penalty := -25.0
testConfidence += penalty
adjustments = append(adjustments, fmt.Sprintf("-25 (night activity %.1f > day %.1f avg/hr)", nightAvg, dayAvg))
}
// Overnight vs afternoon productivity penalty
getActivityInRange := func(startLocalHour, endLocalHour int, includeHalfHour bool) int {
activity := 0
for localHour := startLocalHour; localHour <= endLocalHour; localHour++ {
utcHour := localToUTC(localHour, testOffset)
activity += input.hourCounts[utcHour]
if includeHalfHour && localHour == endLocalHour {
utcHalfHour := float64(localHour-1) + 0.5 - testOffset
for utcHalfHour >= 24 {
utcHalfHour -= 24
}
for utcHalfHour < 0 {
utcHalfHour += 24
}
activity += input.halfHourCounts[utcHalfHour]
}
}
return activity
}
overnightActivity := getActivityInRange(1, 2, true)
afternoonActivity := getActivityInRange(14, 15, true)
if overnightActivity > 10 && overnightActivity > afternoonActivity {
productivityRatio := float64(overnightActivity) / math.Max(float64(afternoonActivity), 1.0)
var penalty float64
switch {
case productivityRatio > 2.0:
penalty = -30.0
adjustments = append(adjustments, fmt.Sprintf("-30 (overnight %d >> afternoon %d events - %.1fx more productive)", overnightActivity, afternoonActivity, productivityRatio))
case productivityRatio > 1.5:
penalty = -15.0
adjustments = append(adjustments, fmt.Sprintf("-15 (overnight %d > afternoon %d events - %.1fx more productive)", overnightActivity, afternoonActivity, productivityRatio))
default:
penalty = -5.0
adjustments = append(adjustments, fmt.Sprintf("-5 (overnight %d > afternoon %d events - %.1fx more productive)", overnightActivity, afternoonActivity, productivityRatio))
}
testConfidence += penalty
}
// Regional timezone pattern recognition
testConfidence, adjustments = applyRegionalPatterns(testOffset, input, testConfidence, adjustments,
lunchLocalStart, testLunchConf, lunchDipStrength, firstActivityLocal, eveningRatio, maxActivityHour)
// European morning activity penalty
if !europeanMorningActivityCheck {
penalty := 15.0
if eveningRatio > 0.1 {
penalty = 5.0
adjustments = append(adjustments, fmt.Sprintf("-%.0f (European timezone but no morning activity, reduced due to %.0f%% evening)", penalty, eveningRatio*100))
} else {
adjustments = append(adjustments, fmt.Sprintf("-%.0f (European timezone but no morning activity)", penalty))
}
testConfidence -= penalty
}
// Profile timezone bonus
isProfileTimezone := false
if input.profileTimezone != "" {
profileOffset := parseUTCOffsetString(input.profileTimezone)
if profileOffset == testOffset {
isProfileTimezone = true
testConfidence += 10.0
adjustments = append(adjustments, "+10 (user's profile timezone)")
}
}
// Unusual lunch time penalties
if testLunchStart >= 0 && testLunchConf > 0.3 {
if lunchLocalStart < 10.5 || lunchLocalStart > 14.5 {
testConfidence -= 10
adjustments = append(adjustments, fmt.Sprintf("-10 (unusual lunch time %.1f)", lunchLocalStart))
}
if lunchLocalStart < 11.0 {
if testOffset >= 10 && testOffset <= 11 {
testConfidence -= 2
adjustments = append(adjustments, fmt.Sprintf("-2 (early lunch at %.1f for %s)", lunchLocalStart, formatUTCOffset(testOffset)))
} else {
testConfidence -= 5
adjustments = append(adjustments, fmt.Sprintf("-5 (lunch before 11am at %.1f)", lunchLocalStart))
}
}
if lunchLocalStart > 15.0 {
testConfidence -= 20
adjustments = append(adjustments, fmt.Sprintf("-20 (lunch after 3pm at %.1f)", lunchLocalStart))
}
}
// Apply developer population weight as a Bayesian prior
// Uses logarithmic scaling: massive populations get significant bonuses,
// tiny populations get heavy penalties. This ensures UTC+8 (China) beats
// adjacent timezones like UTC+9 (Japan) for Chinese developers.
if weight, ok := devPopulationWeight[testOffset]; ok {
// Logarithmic bonus scaled by weight
// weight 10 (China) = +18.5 points
// weight 6 (Japan) = +14.5 points
// weight 0.4 (Adelaide) = -5.9 points (penalty)
popBonus := math.Log(weight+0.1) * 8.0
if popBonus < -3.0 {
popBonus = -3.0 // Cap penalty so rare timezones can compete
}
testConfidence += popBonus
switch {
case weight >= 8.0:
adjustments = append(adjustments, fmt.Sprintf("+%.1f (massive dev population)", popBonus))
case weight >= 4.0:
adjustments = append(adjustments, fmt.Sprintf("+%.1f (large dev population)", popBonus))
case weight >= 1.0:
adjustments = append(adjustments, fmt.Sprintf("+%.1f (moderate dev population)", popBonus))
default:
adjustments = append(adjustments, fmt.Sprintf("+%.1f (small dev population)", popBonus))
}
}
// Scale confidence
testConfidence *= 1.5
candidate := evaluationCandidate{
timezone: formatUTCOffset(testOffset),
offset: testOffset,
confidence: testConfidence,
eveningActivity: eveningActivity,
lunchReasonable: lunchReasonable,
workHoursReasonable: workReasonable,
sleepReasonable: sleepReasonable,
peakTimeReasonable: peakReasonable,
workStartUTC: testActiveStartUTC,
workEndUTC: testActiveEndUTC,
sleepMidUTC: sleepMidUTC,
lunchDipStrength: lunchDipStrength,
lunchStartUTC: testLunchStart,
lunchEndUTC: testLunchEnd,
lunchConfidence: testLunchConf,
scoringDetails: adjustments,
isProfile: isProfileTimezone,
sleepBucketsUTC: sleepBucketsUTC,
peakStartUTC: peakStartUTC,
peakEndUTC: peakEndUTC,
peakCount: peakCount,
}
candidates = append(candidates, candidate)
}
// Sort candidates by confidence (descending)
sort.Slice(candidates, func(i, j int) bool {
if candidates[i].confidence != candidates[j].confidence {
return candidates[i].confidence > candidates[j].confidence
}
return candidates[i].offset < candidates[j].offset
})
// Boost fractional offsets when adjacent integer offsets score well
// This helps rare timezones like Nepal (5.75) compete with India (5.5)
candidates = boostAdjacentFractionalOffsets(candidates)
return candidates
}
// boostAdjacentFractionalOffsets boosts fractional timezone offsets when nearby offsets score well.
// This helps rare timezones like Nepal (UTC+5:45) compete with dominant neighbors like India (UTC+5:30).
// If a user's patterns fit UTC+5 or UTC+6, they could also fit UTC+5:45.
func boostAdjacentFractionalOffsets(candidates []evaluationCandidate) []evaluationCandidate {
// Build offset -> position map for quick lookups
offsetPos := make(map[float64]int)
for i := range candidates {
offsetPos[candidates[i].offset] = i
}
// Find the top 5 confidence threshold
top5Threshold := -1000.0
if len(candidates) >= 5 {
top5Threshold = candidates[4].confidence
}
// Rare offsets that may need boosting when adjacent popular timezones score well
// Includes fractional offsets and isolated integer offsets like Hawaii
rareOffsets := map[float64][]float64{
// Fractional offsets
5.75: {5.5, 6.0, 5.0}, // Nepal: check India (5.5), Bangladesh (6), Pakistan (5)
4.5: {4.0, 5.0}, // Afghanistan: check UAE (4), Pakistan (5)
3.5: {3.0, 4.0}, // Iran: check Moscow (3), UAE (4)
9.5: {9.0, 10.0}, // Adelaide: check Japan (9), Sydney (10)
10.5: {10.0, 11.0}, // Lord Howe Island
6.5: {6.0, 7.0}, // Myanmar: check Bangladesh (6), Thailand (7)
12.75: {12.0, 13.0}, // Chatham Islands
8.75: {8.0, 9.0}, // Eucla
-9.5: {-10.0, -9.0}, // Marquesas
-3.5: {-4.0, -3.0}, // Newfoundland
// Isolated integer offsets that overlap with popular neighbors
-10.0: {-8.0, -7.0, -9.0}, // Hawaii: overlaps with US Pacific/Mountain
-9.0: {-8.0, -7.0}, // Alaska: overlaps with US Pacific/Mountain
-11.0: {-10.0, -12.0}, // American Samoa
}
for fracOffset, adjacentOffsets := range rareOffsets {
fracPos, exists := offsetPos[fracOffset]
if !exists {
continue
}
// Skip if already in top 3 - they don't need boosting
if fracPos < 3 {
continue
}