@tduyng/pi-timbre 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/stats.ts ADDED
@@ -0,0 +1,112 @@
1
+ export function sentences(text: string): string[] {
2
+ return text
3
+ .split(/(?<=[.!?])\s+/)
4
+ .map((s) => s.trim())
5
+ .filter((s) => s.length > 0)
6
+ }
7
+
8
+ export type Burstiness = {
9
+ cov: number
10
+ mean: number
11
+ count: number
12
+ monotoneRun: number
13
+ }
14
+
15
+ export function burstiness(text: string): Burstiness {
16
+ const lens = sentences(text).map((s) => s.split(/\s+/).length)
17
+ const count = lens.length
18
+ if (count === 0) return { cov: 0, mean: 0, count: 0, monotoneRun: 0 }
19
+
20
+ const mean = lens.reduce((a, b) => a + b, 0) / count
21
+ const variance = lens.reduce((a, b) => a + (b - mean) ** 2, 0) / count
22
+ const cov = mean === 0 ? 0 : Math.sqrt(variance) / mean
23
+
24
+ let run = 1
25
+ let best = 1
26
+ for (let i = 1; i < count; i++) {
27
+ run = Math.abs(lens[i]! - lens[i - 1]!) <= 3 ? run + 1 : 1
28
+ best = Math.max(best, run)
29
+ }
30
+
31
+ return { cov, mean, count, monotoneRun: count === 1 ? 1 : best }
32
+ }
33
+
34
+ export function wordCount(text: string): number {
35
+ return text.split(/\s+/).filter((w) => w !== '').length
36
+ }
37
+
38
+ export function isMonotone(b: Burstiness): boolean {
39
+ return b.count >= 6 && b.cov < 0.35
40
+ }
41
+
42
+ export function hasStaccatoRun(text: string): boolean {
43
+ const lens = sentences(text).map((s) => s.split(/\s+/).length)
44
+ let run = 0
45
+ for (const n of lens) {
46
+ run = n < 9 ? run + 1 : 0
47
+ if (run >= 4) return true
48
+ }
49
+ return false
50
+ }
51
+
52
+ export function openingOf(sentence: string): string {
53
+ const words = sentence.toLowerCase().match(/[a-z0-9']+/g) ?? []
54
+ if (words.length < 2) return ''
55
+ return `${words[0]} ${words[1]}`
56
+ }
57
+
58
+ export function repeatedOpening(text: string, run = 3): string | null {
59
+ const openings = sentences(text).map(openingOf)
60
+ let count = 0
61
+ let prev = ''
62
+ for (const o of openings) {
63
+ count = o !== '' && o === prev ? count + 1 : 1
64
+ prev = o
65
+ if (o !== '' && count >= run) return o
66
+ }
67
+ return null
68
+ }
69
+
70
+ const SUBORDINATORS = [
71
+ 'who',
72
+ 'whom',
73
+ 'whose',
74
+ 'which',
75
+ 'although',
76
+ 'because',
77
+ 'since',
78
+ 'whereas',
79
+ 'despite',
80
+ 'unless',
81
+ 'until'
82
+ ] as const
83
+
84
+ const SUBORDINATOR_RE = new RegExp(
85
+ `\\b(?:${SUBORDINATORS.join('|')})\\b|\\bin order to\\b|\\bresulting in\\b|\\bleading to\\b`,
86
+ 'gi'
87
+ )
88
+
89
+ function subordinatorCount(sentence: string): number {
90
+ SUBORDINATOR_RE.lastIndex = 0
91
+ return (sentence.match(SUBORDINATOR_RE) ?? []).length
92
+ }
93
+
94
+ export function hasClauseStack(text: string, perSentence = 3, minSentences = 2): boolean {
95
+ let stacked = 0
96
+ for (const s of sentences(text)) {
97
+ if (subordinatorCount(s) >= perSentence) {
98
+ stacked++
99
+ if (stacked >= minSentences) return true
100
+ }
101
+ }
102
+ return false
103
+ }
104
+ export function hasLongSentenceRun(text: string, limit = 28, run = 3): boolean {
105
+ const lens = sentences(text).map((s) => s.split(/\s+/).length)
106
+ let count = 0
107
+ for (const n of lens) {
108
+ count = n > limit ? count + 1 : 0
109
+ if (count >= run) return true
110
+ }
111
+ return false
112
+ }