chaffjs 0.26.0 → 0.27.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/dist/ai-score/human-shares.js +59 -59
- package/dist/ai-score/human-shares.js.map +1 -1
- package/dist/capitals-with-small.d.ts +8 -0
- package/dist/capitals-with-small.d.ts.map +1 -0
- package/dist/capitals-with-small.js +20 -0
- package/dist/capitals-with-small.js.map +1 -0
- package/dist/derived/relative-weekdays.d.ts +16 -0
- package/dist/derived/relative-weekdays.d.ts.map +1 -0
- package/dist/derived/relative-weekdays.js +52 -0
- package/dist/derived/relative-weekdays.js.map +1 -0
- package/dist/detectors/change-rate.d.ts +3 -0
- package/dist/detectors/change-rate.d.ts.map +1 -0
- package/dist/detectors/change-rate.js +125 -0
- package/dist/detectors/change-rate.js.map +1 -0
- package/dist/detectors/derived-numbers.d.ts.map +1 -1
- package/dist/detectors/derived-numbers.js +3 -7
- package/dist/detectors/derived-numbers.js.map +1 -1
- package/dist/detectors/document-date.d.ts +4 -0
- package/dist/detectors/document-date.d.ts.map +1 -0
- package/dist/detectors/document-date.js +12 -0
- package/dist/detectors/document-date.js.map +1 -0
- package/dist/detectors/orthographic-variant.d.ts.map +1 -1
- package/dist/detectors/orthographic-variant.js +16 -11
- package/dist/detectors/orthographic-variant.js.map +1 -1
- package/dist/detectors/registry/change-rate.d.ts +3 -0
- package/dist/detectors/registry/change-rate.d.ts.map +1 -0
- package/dist/detectors/registry/change-rate.js +3 -0
- package/dist/detectors/registry/change-rate.js.map +1 -0
- package/dist/detectors/registry/version-mismatch.d.ts +3 -0
- package/dist/detectors/registry/version-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/version-mismatch.js +3 -0
- package/dist/detectors/registry/version-mismatch.js.map +1 -0
- package/dist/detectors/relative-dates.d.ts.map +1 -1
- package/dist/detectors/relative-dates.js +3 -1
- package/dist/detectors/relative-dates.js.map +1 -1
- package/dist/detectors/relative-day-words.d.ts +4 -0
- package/dist/detectors/relative-day-words.d.ts.map +1 -0
- package/dist/detectors/relative-day-words.js +105 -0
- package/dist/detectors/relative-day-words.js.map +1 -0
- package/dist/detectors/structure-tree.d.ts +4 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +65 -3
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/version-mismatch.d.ts +3 -0
- package/dist/detectors/version-mismatch.d.ts.map +1 -0
- package/dist/detectors/version-mismatch.js +19 -0
- package/dist/detectors/version-mismatch.js.map +1 -0
- package/dist/structure/change-rate.d.ts +51 -0
- package/dist/structure/change-rate.d.ts.map +1 -0
- package/dist/structure/change-rate.js +171 -0
- package/dist/structure/change-rate.js.map +1 -0
- package/dist/structure/list-numbering.d.ts +17 -0
- package/dist/structure/list-numbering.d.ts.map +1 -0
- package/dist/structure/list-numbering.js +27 -0
- package/dist/structure/list-numbering.js.map +1 -0
- package/dist/structure/percent-sum-prose.d.ts +7 -0
- package/dist/structure/percent-sum-prose.d.ts.map +1 -0
- package/dist/structure/percent-sum-prose.js +44 -0
- package/dist/structure/percent-sum-prose.js.map +1 -0
- package/dist/structure/version-mismatch.d.ts +23 -0
- package/dist/structure/version-mismatch.d.ts.map +1 -0
- package/dist/structure/version-mismatch.js +74 -0
- package/dist/structure/version-mismatch.js.map +1 -0
- package/dist/structure/weekday.d.ts +21 -2
- package/dist/structure/weekday.d.ts.map +1 -1
- package/dist/structure/weekday.js +70 -8
- package/dist/structure/weekday.js.map +1 -1
- package/package.json +3 -3
- package/rules/agreement-slip.yaml +2 -2
- package/rules/change-rate-mismatch.yaml +44 -0
- package/rules/date-weekday-mismatch.yaml +4 -4
- package/rules/max-sentence-length.yaml +1 -0
- package/rules/numbering-gap.yaml +4 -4
- package/rules/one-sentence-paragraph-run.yaml +1 -0
- package/rules/orthographic-variant.yaml +3 -3
- package/rules/percent-sum-mismatch.yaml +4 -4
- package/rules/relative-date-mismatch.yaml +4 -4
- package/rules/undefined-acronym.yaml +1 -0
- package/rules/version-mismatch.yaml +77 -0
- package/src/ai-score/human-shares.ts +59 -59
- package/src/capitals-with-small.ts +23 -0
- package/src/derived/relative-weekdays.ts +65 -0
- package/src/detectors/change-rate.ts +153 -0
- package/src/detectors/derived-numbers.ts +3 -11
- package/src/detectors/document-date.ts +18 -0
- package/src/detectors/orthographic-variant.ts +8 -4
- package/src/detectors/registry/change-rate.ts +4 -0
- package/src/detectors/registry/version-mismatch.ts +4 -0
- package/src/detectors/relative-dates.ts +3 -1
- package/src/detectors/relative-day-words.ts +126 -0
- package/src/detectors/structure-tree.ts +76 -4
- package/src/detectors/version-mismatch.ts +23 -0
- package/src/structure/change-rate.ts +223 -0
- package/src/structure/list-numbering.ts +34 -0
- package/src/structure/percent-sum-prose.ts +54 -0
- package/src/structure/version-mismatch.ts +103 -0
- package/src/structure/weekday.ts +83 -6
|
@@ -4,47 +4,47 @@ import type { HumanShares } from "./score.ts";
|
|
|
4
4
|
/** On how many human documents of each genre group each AI-shape rule reported, at the genre's level. */
|
|
5
5
|
export const HUMAN_SHARES: HumanShares = {
|
|
6
6
|
"ai-tell": {
|
|
7
|
-
academic: { documents:
|
|
7
|
+
academic: { documents: 10, fired: 0 },
|
|
8
8
|
blog: { documents: 23, fired: 0 },
|
|
9
|
-
business: { documents:
|
|
10
|
-
docs: { documents:
|
|
9
|
+
business: { documents: 80, fired: 0 },
|
|
10
|
+
docs: { documents: 21, fired: 0 },
|
|
11
11
|
legal: { documents: 35, fired: 0 },
|
|
12
12
|
literature: { documents: 13, fired: 0 },
|
|
13
13
|
speech: { documents: 7, fired: 0 },
|
|
14
14
|
technical: { documents: 26, fired: 0 },
|
|
15
15
|
},
|
|
16
16
|
"announcing-opener": {
|
|
17
|
-
academic: { documents:
|
|
17
|
+
academic: { documents: 10, fired: 0 },
|
|
18
18
|
blog: { documents: 23, fired: 0 },
|
|
19
|
-
business: { documents:
|
|
20
|
-
docs: { documents:
|
|
19
|
+
business: { documents: 80, fired: 0 },
|
|
20
|
+
docs: { documents: 21, fired: 0 },
|
|
21
21
|
legal: { documents: 35, fired: 0 },
|
|
22
22
|
literature: { documents: 13, fired: 0 },
|
|
23
23
|
speech: { documents: 7, fired: 0 },
|
|
24
24
|
technical: { documents: 26, fired: 0 },
|
|
25
25
|
},
|
|
26
26
|
"assistant-residue": {
|
|
27
|
-
academic: { documents:
|
|
27
|
+
academic: { documents: 10, fired: 0 },
|
|
28
28
|
blog: { documents: 23, fired: 0 },
|
|
29
|
-
business: { documents:
|
|
30
|
-
docs: { documents:
|
|
29
|
+
business: { documents: 80, fired: 0 },
|
|
30
|
+
docs: { documents: 21, fired: 0 },
|
|
31
31
|
legal: { documents: 35, fired: 0 },
|
|
32
32
|
literature: { documents: 13, fired: 0 },
|
|
33
33
|
speech: { documents: 7, fired: 0 },
|
|
34
|
-
technical: { documents:
|
|
34
|
+
technical: { documents: 49, fired: 0 },
|
|
35
35
|
},
|
|
36
36
|
"bold-density": {
|
|
37
|
-
academic: { documents:
|
|
37
|
+
academic: { documents: 10, fired: 0 },
|
|
38
38
|
blog: { documents: 23, fired: 0 },
|
|
39
|
-
business: { documents:
|
|
40
|
-
docs: { documents:
|
|
39
|
+
business: { documents: 80, fired: 1 },
|
|
40
|
+
docs: { documents: 21, fired: 0 },
|
|
41
41
|
legal: { documents: 35, fired: 0 },
|
|
42
42
|
literature: { documents: 13, fired: 0 },
|
|
43
43
|
speech: { documents: 7, fired: 0 },
|
|
44
|
-
technical: { documents:
|
|
44
|
+
technical: { documents: 49, fired: 2 },
|
|
45
45
|
},
|
|
46
46
|
"bold-label-list": {
|
|
47
|
-
academic: { documents:
|
|
47
|
+
academic: { documents: 5, fired: 0 },
|
|
48
48
|
blog: { documents: 14, fired: 0 },
|
|
49
49
|
business: { documents: 39, fired: 0 },
|
|
50
50
|
docs: { documents: 10, fired: 0 },
|
|
@@ -54,25 +54,25 @@ export const HUMAN_SHARES: HumanShares = {
|
|
|
54
54
|
technical: { documents: 13, fired: 1 },
|
|
55
55
|
},
|
|
56
56
|
"chat-citation-residue": {
|
|
57
|
-
academic: { documents:
|
|
57
|
+
academic: { documents: 10, fired: 0 },
|
|
58
58
|
blog: { documents: 23, fired: 0 },
|
|
59
|
-
business: { documents:
|
|
60
|
-
docs: { documents:
|
|
59
|
+
business: { documents: 80, fired: 0 },
|
|
60
|
+
docs: { documents: 21, fired: 0 },
|
|
61
61
|
legal: { documents: 35, fired: 0 },
|
|
62
62
|
literature: { documents: 13, fired: 0 },
|
|
63
63
|
speech: { documents: 7, fired: 0 },
|
|
64
|
-
technical: { documents:
|
|
64
|
+
technical: { documents: 49, fired: 0 },
|
|
65
65
|
},
|
|
66
66
|
"closing-cliche": {
|
|
67
|
-
academic: { documents:
|
|
67
|
+
academic: { documents: 10, fired: 0 },
|
|
68
68
|
blog: { documents: 23, fired: 0 },
|
|
69
|
-
docs: { documents:
|
|
69
|
+
docs: { documents: 21, fired: 0 },
|
|
70
70
|
legal: { documents: 35, fired: 1 },
|
|
71
71
|
literature: { documents: 13, fired: 0 },
|
|
72
72
|
speech: { documents: 7, fired: 0 },
|
|
73
73
|
},
|
|
74
74
|
"colon-lead-in": {
|
|
75
|
-
academic: { documents:
|
|
75
|
+
academic: { documents: 5, fired: 0 },
|
|
76
76
|
blog: { documents: 14, fired: 0 },
|
|
77
77
|
business: { documents: 39, fired: 0 },
|
|
78
78
|
docs: { documents: 10, fired: 0 },
|
|
@@ -82,110 +82,110 @@ export const HUMAN_SHARES: HumanShares = {
|
|
|
82
82
|
technical: { documents: 13, fired: 1 },
|
|
83
83
|
},
|
|
84
84
|
"contrast-framing": {
|
|
85
|
-
academic: { documents:
|
|
85
|
+
academic: { documents: 10, fired: 0 },
|
|
86
86
|
blog: { documents: 23, fired: 0 },
|
|
87
|
-
business: { documents:
|
|
88
|
-
docs: { documents:
|
|
87
|
+
business: { documents: 80, fired: 0 },
|
|
88
|
+
docs: { documents: 21, fired: 0 },
|
|
89
89
|
legal: { documents: 35, fired: 0 },
|
|
90
90
|
literature: { documents: 13, fired: 0 },
|
|
91
91
|
speech: { documents: 7, fired: 0 },
|
|
92
92
|
technical: { documents: 26, fired: 0 },
|
|
93
93
|
},
|
|
94
94
|
"emoji-density": {
|
|
95
|
-
academic: { documents:
|
|
95
|
+
academic: { documents: 10, fired: 0 },
|
|
96
96
|
blog: { documents: 23, fired: 0 },
|
|
97
|
-
business: { documents:
|
|
98
|
-
docs: { documents:
|
|
97
|
+
business: { documents: 80, fired: 0 },
|
|
98
|
+
docs: { documents: 21, fired: 0 },
|
|
99
99
|
legal: { documents: 35, fired: 0 },
|
|
100
100
|
literature: { documents: 13, fired: 0 },
|
|
101
101
|
speech: { documents: 7, fired: 0 },
|
|
102
|
-
technical: { documents:
|
|
102
|
+
technical: { documents: 49, fired: 0 },
|
|
103
103
|
},
|
|
104
104
|
"emoji-heading": {
|
|
105
|
-
academic: { documents:
|
|
105
|
+
academic: { documents: 10, fired: 0 },
|
|
106
106
|
blog: { documents: 23, fired: 0 },
|
|
107
|
-
business: { documents:
|
|
108
|
-
docs: { documents:
|
|
107
|
+
business: { documents: 78, fired: 1 },
|
|
108
|
+
docs: { documents: 21, fired: 0 },
|
|
109
109
|
legal: { documents: 28, fired: 0 },
|
|
110
110
|
literature: { documents: 4, fired: 0 },
|
|
111
111
|
speech: { documents: 7, fired: 0 },
|
|
112
112
|
},
|
|
113
113
|
"figurative-density": {
|
|
114
|
-
academic: { documents:
|
|
114
|
+
academic: { documents: 10, fired: 0 },
|
|
115
115
|
blog: { documents: 23, fired: 0 },
|
|
116
|
-
business: { documents:
|
|
117
|
-
docs: { documents:
|
|
116
|
+
business: { documents: 80, fired: 0 },
|
|
117
|
+
docs: { documents: 21, fired: 0 },
|
|
118
118
|
legal: { documents: 35, fired: 0 },
|
|
119
119
|
literature: { documents: 13, fired: 0 },
|
|
120
120
|
speech: { documents: 7, fired: 0 },
|
|
121
|
-
technical: { documents:
|
|
121
|
+
technical: { documents: 49, fired: 0 },
|
|
122
122
|
},
|
|
123
123
|
"no-em-dash": {
|
|
124
|
-
academic: { documents:
|
|
124
|
+
academic: { documents: 10, fired: 0 },
|
|
125
125
|
blog: { documents: 23, fired: 0 },
|
|
126
|
-
business: { documents:
|
|
127
|
-
docs: { documents:
|
|
126
|
+
business: { documents: 80, fired: 2 },
|
|
127
|
+
docs: { documents: 21, fired: 0 },
|
|
128
128
|
legal: { documents: 35, fired: 2 },
|
|
129
129
|
literature: { documents: 13, fired: 2 },
|
|
130
130
|
speech: { documents: 7, fired: 0 },
|
|
131
|
-
technical: { documents:
|
|
131
|
+
technical: { documents: 49, fired: 0 },
|
|
132
132
|
},
|
|
133
133
|
"padded-intro": {
|
|
134
|
-
academic: { documents:
|
|
134
|
+
academic: { documents: 10, fired: 0 },
|
|
135
135
|
blog: { documents: 23, fired: 0 },
|
|
136
|
-
business: { documents:
|
|
137
|
-
docs: { documents:
|
|
136
|
+
business: { documents: 47, fired: 1 },
|
|
137
|
+
docs: { documents: 21, fired: 0 },
|
|
138
138
|
legal: { documents: 35, fired: 0 },
|
|
139
139
|
literature: { documents: 13, fired: 0 },
|
|
140
140
|
speech: { documents: 7, fired: 0 },
|
|
141
141
|
},
|
|
142
142
|
"paragraph-length-variance": {
|
|
143
|
-
academic: { documents:
|
|
143
|
+
academic: { documents: 10, fired: 0 },
|
|
144
144
|
blog: { documents: 23, fired: 0 },
|
|
145
|
-
docs: { documents:
|
|
145
|
+
docs: { documents: 21, fired: 0 },
|
|
146
146
|
legal: { documents: 35, fired: 0 },
|
|
147
147
|
literature: { documents: 13, fired: 1 },
|
|
148
148
|
speech: { documents: 7, fired: 0 },
|
|
149
149
|
},
|
|
150
150
|
"rule-of-three": {
|
|
151
|
-
academic: { documents:
|
|
151
|
+
academic: { documents: 10, fired: 0 },
|
|
152
152
|
blog: { documents: 23, fired: 0 },
|
|
153
|
-
docs: { documents:
|
|
153
|
+
docs: { documents: 21, fired: 0 },
|
|
154
154
|
legal: { documents: 35, fired: 0 },
|
|
155
155
|
literature: { documents: 13, fired: 0 },
|
|
156
156
|
speech: { documents: 7, fired: 0 },
|
|
157
157
|
technical: { documents: 26, fired: 0 },
|
|
158
158
|
},
|
|
159
159
|
"section-length-uniformity": {
|
|
160
|
-
academic: { documents:
|
|
160
|
+
academic: { documents: 10, fired: 0 },
|
|
161
161
|
blog: { documents: 23, fired: 0 },
|
|
162
|
-
docs: { documents:
|
|
162
|
+
docs: { documents: 21, fired: 0 },
|
|
163
163
|
legal: { documents: 35, fired: 0 },
|
|
164
164
|
literature: { documents: 13, fired: 1 },
|
|
165
165
|
speech: { documents: 7, fired: 0 },
|
|
166
166
|
technical: { documents: 26, fired: 0 },
|
|
167
167
|
},
|
|
168
168
|
"sentence-rhythm": {
|
|
169
|
-
academic: { documents:
|
|
169
|
+
academic: { documents: 10, fired: 0 },
|
|
170
170
|
blog: { documents: 23, fired: 0 },
|
|
171
|
-
docs: { documents:
|
|
171
|
+
docs: { documents: 21, fired: 0 },
|
|
172
172
|
legal: { documents: 35, fired: 0 },
|
|
173
173
|
literature: { documents: 13, fired: 1 },
|
|
174
174
|
speech: { documents: 7, fired: 0 },
|
|
175
175
|
technical: { documents: 26, fired: 1 },
|
|
176
176
|
},
|
|
177
177
|
"stock-transition": {
|
|
178
|
-
academic: { documents:
|
|
178
|
+
academic: { documents: 10, fired: 0 },
|
|
179
179
|
blog: { documents: 23, fired: 0 },
|
|
180
|
-
business: { documents:
|
|
181
|
-
docs: { documents:
|
|
180
|
+
business: { documents: 80, fired: 0 },
|
|
181
|
+
docs: { documents: 21, fired: 0 },
|
|
182
182
|
legal: { documents: 35, fired: 0 },
|
|
183
183
|
literature: { documents: 13, fired: 0 },
|
|
184
184
|
speech: { documents: 7, fired: 0 },
|
|
185
185
|
technical: { documents: 26, fired: 0 },
|
|
186
186
|
},
|
|
187
187
|
"translationese-density": {
|
|
188
|
-
academic: { documents:
|
|
188
|
+
academic: { documents: 5, fired: 0 },
|
|
189
189
|
blog: { documents: 14, fired: 0 },
|
|
190
190
|
business: { documents: 39, fired: 0 },
|
|
191
191
|
docs: { documents: 10, fired: 0 },
|
|
@@ -195,13 +195,13 @@ export const HUMAN_SHARES: HumanShares = {
|
|
|
195
195
|
technical: { documents: 22, fired: 0 },
|
|
196
196
|
},
|
|
197
197
|
"unfilled-placeholder": {
|
|
198
|
-
academic: { documents:
|
|
198
|
+
academic: { documents: 10, fired: 0 },
|
|
199
199
|
blog: { documents: 23, fired: 0 },
|
|
200
|
-
business: { documents:
|
|
201
|
-
docs: { documents:
|
|
200
|
+
business: { documents: 80, fired: 1 },
|
|
201
|
+
docs: { documents: 21, fired: 0 },
|
|
202
202
|
legal: { documents: 35, fired: 0 },
|
|
203
203
|
literature: { documents: 13, fired: 0 },
|
|
204
204
|
speech: { documents: 7, fired: 0 },
|
|
205
|
-
technical: { documents:
|
|
205
|
+
technical: { documents: 49, fired: 0 },
|
|
206
206
|
},
|
|
207
207
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Words that start with two capitals or more and go on in small letters, where the capitals are not a spelling of the
|
|
2
|
+
// plain word: a name in capitals with an English ending (SENDs, OPENed), or the letters of an acronym picked out in its
|
|
3
|
+
// expansion (CLImatology and PERsistence for CLIPER: part of a longer acronym). A name written that way (HBase, RSpec,
|
|
4
|
+
// SCIMple beside SCIM) is a spelling, and stays one.
|
|
5
|
+
|
|
6
|
+
const CAPITALS_THEN_SMALL = /^(\p{Lu}{2,})\p{Ll}+$/u;
|
|
7
|
+
const CAPITALS_WITH_ENDING = /^\p{Lu}{2,}(?:s|es|d|ed|ing)$/u;
|
|
8
|
+
const CAPITALS_ONLY = /^\p{Lu}[\p{Lu}\d]+$/u;
|
|
9
|
+
const LATIN_RUN = /[\p{L}\d]+/gu;
|
|
10
|
+
|
|
11
|
+
/** The words of a stretch of text written in capitals only (CLIPER, CAMEX), which a picked-out expansion spells. */
|
|
12
|
+
export const acronymsIn = (text: string): string[] => [...text.matchAll(LATIN_RUN)].map((match) => match[0]).filter((word) => CAPITALS_ONLY.test(word));
|
|
13
|
+
|
|
14
|
+
const isPickedOut = (word: string, acronyms: readonly string[]): boolean => {
|
|
15
|
+
const capitals = CAPITALS_THEN_SMALL.exec(word)?.[1];
|
|
16
|
+
return capitals !== undefined && acronyms.some((acronym) => acronym.length > capitals.length && acronym.includes(capitals));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether a word is capitals with an ending, or the letters of an acronym among acronyms. A hyphenated word is compared
|
|
21
|
+
* as written: leaving it out for one part would hide a variant in another (GitHub-SENDs, Github-SENDs).
|
|
22
|
+
*/
|
|
23
|
+
export const isCapitalsNotSpelling = (word: string, acronyms: readonly string[]): boolean => CAPITALS_WITH_ENDING.test(word) || isPickedOut(word, acronyms);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// A date named by a weekday or a day relative to the document's date (「来週月曜(10月12日)」"next Monday (October 12)",
|
|
2
|
+
// 「明日(10月7日)」) that the absolute date beside it contradicts. Pure; the detector finds the words and the dates.
|
|
3
|
+
import type { Span } from "../plugin.ts";
|
|
4
|
+
|
|
5
|
+
/** 「来週月曜」"next Monday": weeks from the base's week (来週 is 1), and the weekday (Sunday is 0). */
|
|
6
|
+
export type WeekdayRelative = Span & { readonly kind: "weekday"; readonly weeks: number; readonly weekday: number };
|
|
7
|
+
/** 「明日」"tomorrow": days from the base. */
|
|
8
|
+
export type DayRelative = Span & { readonly kind: "day"; readonly days: number };
|
|
9
|
+
export type DayWord = WeekdayRelative | DayRelative;
|
|
10
|
+
|
|
11
|
+
const DAY_MS = 86_400_000;
|
|
12
|
+
const WEEK = 7;
|
|
13
|
+
const MONDAY = 1;
|
|
14
|
+
const FULL = /^(\d{4})-(\d{2})-(\d{2})$/u;
|
|
15
|
+
const MONTH_DAY = /^(\d{2})-(\d{2})$/u;
|
|
16
|
+
|
|
17
|
+
const timeOf = (value: string): number | undefined => {
|
|
18
|
+
const found = FULL.exec(value);
|
|
19
|
+
if (found === null) return undefined;
|
|
20
|
+
const [year, month, day] = [Number(found[1]), Number(found[2]), Number(found[3])];
|
|
21
|
+
const time = Date.UTC(year, month - 1, day);
|
|
22
|
+
const date = new Date(time);
|
|
23
|
+
return date.getUTCMonth() === month - 1 && date.getUTCDate() === day ? time : undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const valueOf = (time: number): string => new Date(time).toISOString().slice(0, "2026-10-06".length);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The days a weekday word can mean. Weeks are counted from Monday or from Sunday, and "next Monday" can also mean the
|
|
30
|
+
* coming Monday: every reading is allowed, so only a date that fits none of them is wrong.
|
|
31
|
+
*/
|
|
32
|
+
const weekdayCandidates = (base: number, relative: WeekdayRelative): number[] => {
|
|
33
|
+
const baseDay = new Date(base).getUTCDay();
|
|
34
|
+
const fromMonday = base - ((baseDay - MONDAY + WEEK) % WEEK) * DAY_MS;
|
|
35
|
+
const fromSunday = base - baseDay * DAY_MS;
|
|
36
|
+
const inWeek = (weekStart: number, firstDay: number): number => weekStart + (relative.weeks * WEEK + ((relative.weekday - firstDay + WEEK) % WEEK)) * DAY_MS;
|
|
37
|
+
// "next Monday" and "this Monday" can mean the coming one; "last Monday" the one just past.
|
|
38
|
+
const coming = base + (((relative.weekday - baseDay + WEEK - 1) % WEEK) + 1) * DAY_MS;
|
|
39
|
+
const past = base - (((baseDay - relative.weekday + WEEK - 1) % WEEK) + 1) * DAY_MS;
|
|
40
|
+
const nearest = new Map([
|
|
41
|
+
[0, [coming]],
|
|
42
|
+
[1, [coming]],
|
|
43
|
+
[-1, [past]],
|
|
44
|
+
]).get(relative.weeks);
|
|
45
|
+
return [inWeek(fromMonday, MONDAY), inWeek(fromSunday, 0), ...(nearest ?? [])];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const candidatesOf = (base: number, relative: DayWord): number[] =>
|
|
49
|
+
relative.kind === "day" ? [base + relative.days * DAY_MS] : weekdayCandidates(base, relative);
|
|
50
|
+
|
|
51
|
+
/** Whether the written date (2026-10-12, or 10-12 without its year) is one of the days the word can mean. */
|
|
52
|
+
const fits = (written: string, candidates: readonly number[]): boolean => {
|
|
53
|
+
const monthDay = MONTH_DAY.exec(written);
|
|
54
|
+
return candidates.some((time) => (monthDay === null ? valueOf(time) === written : valueOf(time).slice("2026-".length) === written));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** The day the word means, as the message shows it: the first reading, written as the date was (with or without the year). */
|
|
58
|
+
export const relativeDayMismatch = (base: string, relative: DayWord, written: string): string | undefined => {
|
|
59
|
+
const baseTime = timeOf(base);
|
|
60
|
+
if (baseTime === undefined || (!FULL.test(written) && !MONTH_DAY.test(written))) return undefined;
|
|
61
|
+
const candidates = candidatesOf(baseTime, relative);
|
|
62
|
+
const [first] = candidates;
|
|
63
|
+
if (first === undefined || fits(written, candidates)) return undefined;
|
|
64
|
+
return MONTH_DAY.test(written) ? valueOf(first).slice("2026-".length) : valueOf(first);
|
|
65
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// change-rate-mismatch: the reading half. Builds the figures, the rates and the words of structure/change-rate.ts from the
|
|
2
|
+
// tree's quantities, the numbers counted with a word ("1,200 companies"), the calendar years, and the lexicons change-direction,
|
|
3
|
+
// change-base, change-target, change-break, percent-unit and amount-multiplier.
|
|
4
|
+
import type { Detector, Finding, LexiconEntry, ProseDocument, Span, StructureNode } from "../plugin.ts";
|
|
5
|
+
import { inDocumentOrder } from "../structure/issues.ts";
|
|
6
|
+
import { changeRateMismatches, type BaseMark, type Break, type Direction, type Figure, type Period, type Rate } from "../structure/change-rate.ts";
|
|
7
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
8
|
+
import { quoteAt } from "./structure-tree.ts";
|
|
9
|
+
|
|
10
|
+
const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
11
|
+
|
|
12
|
+
const DIGITS = /\d[\d,]*(?:\.(\d+))?/u;
|
|
13
|
+
const DECIMAL_BASE = 10;
|
|
14
|
+
const LATIN = /^[A-Za-z]/u;
|
|
15
|
+
|
|
16
|
+
/** The step a number is written to: its last digit, times the magnitude the tree folded into the value (12億 is 12 × 1億). */
|
|
17
|
+
const stepOf = (written: string, value: number): number | undefined => {
|
|
18
|
+
const found = DIGITS.exec(written.normalize("NFKC"));
|
|
19
|
+
const shown = found === null ? Number.NaN : Number(found[0].replaceAll(",", ""));
|
|
20
|
+
if (!Number.isFinite(shown) || shown === 0) return undefined;
|
|
21
|
+
return (value / shown) * DECIMAL_BASE ** -(found?.[1]?.length ?? 0);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** A word of magnitude right after the number ("$12 million"): the values compared must be written to the same one. */
|
|
25
|
+
const WORD_AFTER = /^\s+([A-Za-z]+)/u;
|
|
26
|
+
const WORDS_AFTER = /^\s+([A-Za-z]+)(?:\s+([A-Za-z]+))?/u;
|
|
27
|
+
|
|
28
|
+
const multiplierAfter = (source: string, end: number, multipliers: readonly string[]): string => {
|
|
29
|
+
const after = WORD_AFTER.exec(source.slice(end))?.[1]?.toLowerCase() ?? "";
|
|
30
|
+
return multipliers.includes(after) ? after : "";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const quantityNodes = (tree: StructureNode): StructureNode[] => inDocumentOrder(tree).filter((node) => node.kind === "quantity" || node.kind === "date");
|
|
34
|
+
|
|
35
|
+
const ratesOf = (doc: ProseDocument, nodes: readonly StructureNode[], units: readonly string[]): Rate[] =>
|
|
36
|
+
nodes.flatMap((node) => {
|
|
37
|
+
const unit = String(node.attrs["unit"] ?? "");
|
|
38
|
+
if (node.kind !== "quantity" || !units.includes(unit)) return [];
|
|
39
|
+
const decimals = DIGITS.exec(doc.source.slice(node.span.start, node.span.end).normalize("NFKC"))?.[1]?.length ?? 0;
|
|
40
|
+
return [{ start: node.span.start, end: node.span.end, value: Number(node.attrs["value"]), decimals }];
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const treeFigures = (doc: ProseDocument, nodes: readonly StructureNode[], units: readonly string[], multipliers: readonly string[]): Figure[] =>
|
|
44
|
+
nodes.flatMap((node) => {
|
|
45
|
+
const unit = String(node.attrs["unit"] ?? "");
|
|
46
|
+
const value = Number(node.attrs["value"]);
|
|
47
|
+
const step = stepOf(doc.source.slice(node.span.start, node.span.end), value);
|
|
48
|
+
if (node.kind !== "quantity" || units.includes(unit) || step === undefined) return [];
|
|
49
|
+
return [{ start: node.span.start, end: node.span.end, value, step, unit: `${unit}|${multiplierAfter(doc.source, node.span.end, multipliers)}` }];
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** A number with no unit the tree reads, counted with the word after it ("1,200 companies", "2.4 million users"). */
|
|
53
|
+
const COUNTED = /(?<![\w.,$€£¥])(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d+))?(?=\s+[A-Za-z])/gu;
|
|
54
|
+
/** A year written alone ("2024 revenue"): a point in time, not a value. */
|
|
55
|
+
const YEAR = /^(?:1[89]|2[01])\d{2}$/u;
|
|
56
|
+
|
|
57
|
+
/** The unit a counted number is compared in: the word after it, with a word of magnitude before that word ("million users"). */
|
|
58
|
+
const countedUnit = (source: string, end: number, multipliers: readonly string[]): string | undefined => {
|
|
59
|
+
const words = WORDS_AFTER.exec(source.slice(end));
|
|
60
|
+
const first = words?.[1]?.toLowerCase() ?? "";
|
|
61
|
+
const multiplier = multipliers.includes(first) ? first : "";
|
|
62
|
+
const noun = multiplier === "" ? first : (words?.[2]?.toLowerCase() ?? "");
|
|
63
|
+
return LATIN.test(noun) ? `${noun}|${multiplier}` : undefined;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const countedFigures = (doc: ProseDocument, taken: readonly Span[], multipliers: readonly string[]): Figure[] =>
|
|
67
|
+
[...doc.source.matchAll(COUNTED)].flatMap((match) => {
|
|
68
|
+
const start = match.index;
|
|
69
|
+
const end = start + match[0].length;
|
|
70
|
+
const unit = countedUnit(doc.source, end, multipliers);
|
|
71
|
+
if (unit === undefined || taken.some((span) => span.start < end && start < span.end) || YEAR.test(match[0])) return [];
|
|
72
|
+
const decimals = match[2] ?? "";
|
|
73
|
+
const value = Number([(match[1] ?? "").replaceAll(",", ""), decimals].filter((part) => part !== "").join("."));
|
|
74
|
+
return [{ start, end, value, step: DECIMAL_BASE ** -decimals.length, unit }];
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** Where a word of the lexicon stands in the source: a Latin word as a whole word, anything else as written. */
|
|
78
|
+
const spansOf = (source: string, word: string): Span[] => {
|
|
79
|
+
const pattern = LATIN.test(word) ? new RegExp(`\\b${escapeRegExp(word)}\\b`, "giu") : new RegExp(escapeRegExp(word), "gu");
|
|
80
|
+
return [...source.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length }));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const SIGNS: Readonly<Record<string, 1 | -1>> = { rise: 1, fall: -1 };
|
|
84
|
+
|
|
85
|
+
/** Where the words of a lexicon stand, less those inside a phrase of its "-not" lexicon ("up to 25%" is a ceiling). */
|
|
86
|
+
const spansWithout = (doc: ProseDocument, entries: readonly LexiconEntry[], notLexicon: string): (Span & { readonly entry: LexiconEntry })[] => {
|
|
87
|
+
const not = patternsOf(doc, notLexicon).flatMap((phrase) => spansOf(doc.source, phrase));
|
|
88
|
+
return entries.flatMap((entry) =>
|
|
89
|
+
spansOf(doc.source, entry.pattern)
|
|
90
|
+
.filter((span) => !not.some((phrase) => phrase.start <= span.start && span.end <= phrase.end))
|
|
91
|
+
.map((span) => ({ ...span, entry })),
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Lexicon change-direction, less the phrases of change-direction-not. */
|
|
96
|
+
const directionsOf = (doc: ProseDocument): Direction[] =>
|
|
97
|
+
spansWithout(doc, doc.lexicons["change-direction"] ?? [], "change-direction-not").flatMap(({ start, end, entry }) => {
|
|
98
|
+
const sign = SIGNS[entry.group ?? ""];
|
|
99
|
+
return sign === undefined ? [] : [{ start, end, sign }];
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
/** Lexicon change-break (「は」, "and"), less the phrases of change-break-not (「上がり」 holds が but starts no subject). */
|
|
103
|
+
const breaksOf = (doc: ProseDocument): Break[] =>
|
|
104
|
+
spansWithout(doc, doc.lexicons["change-break"] ?? [], "change-break-not").map(({ start, end, entry }) => ({
|
|
105
|
+
start,
|
|
106
|
+
end,
|
|
107
|
+
beforeRateOnly: entry.group === "rate",
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
/** A calendar year written alone or with a unit of calendar year (2025, 2025年, 2025年度), and not part of an amount ($2025). */
|
|
111
|
+
const CALENDAR_YEAR = /(?<!\d|\d[.,])(?:1[89]|2[01])\d{2}(?!\d|[.,]\d)/gu;
|
|
112
|
+
|
|
113
|
+
const periodsOf = (doc: ProseDocument, figures: readonly Figure[]): Period[] => {
|
|
114
|
+
const yearUnits = new Set(patternsOf(doc, "calendar-year-unit").map((unit) => `${unit}|`));
|
|
115
|
+
const amounts = figures.filter((figure) => !yearUnits.has(figure.unit));
|
|
116
|
+
return [...doc.source.matchAll(CALENDAR_YEAR)].flatMap((match) => {
|
|
117
|
+
const span = { start: match.index, end: match.index + match[0].length };
|
|
118
|
+
return amounts.some((figure) => figure.start < span.end && span.start < figure.end) ? [] : [{ ...span, year: Number(match[0]) }];
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const marksOf = (doc: ProseDocument, lexicon: string): BaseMark[] =>
|
|
123
|
+
(doc.lexicons[lexicon] ?? []).flatMap((entry) =>
|
|
124
|
+
entry.position === undefined ? [] : spansOf(doc.source, entry.pattern).map((span) => ({ ...span, position: entry.position ?? "before" })),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
export const changeRate: Detector = (doc): Finding[] => {
|
|
128
|
+
if (doc.structure === undefined) return [];
|
|
129
|
+
const nodes = quantityNodes(doc.structure);
|
|
130
|
+
const units = patternsOf(doc, "percent-unit");
|
|
131
|
+
const multipliers = patternsOf(doc, "amount-multiplier").map((word) => word.toLowerCase());
|
|
132
|
+
const taken = nodes.map((node) => node.span);
|
|
133
|
+
const figures = [...treeFigures(doc, nodes, units, multipliers), ...countedFigures(doc, taken, multipliers)];
|
|
134
|
+
const text = {
|
|
135
|
+
sentences: doc.sentences.map((sentence) => sentence.span),
|
|
136
|
+
figures,
|
|
137
|
+
rates: ratesOf(doc, nodes, units),
|
|
138
|
+
directions: directionsOf(doc),
|
|
139
|
+
marks: marksOf(doc, "change-base"),
|
|
140
|
+
targets: marksOf(doc, "change-target"),
|
|
141
|
+
periods: periodsOf(doc, figures),
|
|
142
|
+
breaks: breaksOf(doc),
|
|
143
|
+
source: doc.source,
|
|
144
|
+
};
|
|
145
|
+
return changeRateMismatches(text).map((issue) => ({
|
|
146
|
+
rule: "change-rate-mismatch",
|
|
147
|
+
severity: "warning",
|
|
148
|
+
line: 0,
|
|
149
|
+
column: 0,
|
|
150
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
151
|
+
values: { ...issue.values, offset: issue.offset },
|
|
152
|
+
}));
|
|
153
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Detector, Finding, ProseDocument, Span, StructureNode } from "../plugin.ts";
|
|
2
2
|
import { inDocumentOrder } from "../structure/issues.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { documentDateOf } from "./document-date.ts";
|
|
4
4
|
import { overlapsAny, spanIndex } from "../compare/spans.ts";
|
|
5
5
|
import { yearOf, type DurationUnit } from "../derived/date-arithmetic.ts";
|
|
6
6
|
import { durationMismatches, type DatedValue, type Duration } from "../derived/durations.ts";
|
|
@@ -142,16 +142,8 @@ const markedAges = (doc: ProseDocument): Elapsed[] =>
|
|
|
142
142
|
|
|
143
143
|
/** 文書の日付の年: 日付だけの段落か「更新日:」の段落(date-stamp)の、最初の日付。 */
|
|
144
144
|
const referenceYear = (doc: ProseDocument, tree: StructureNode): number | undefined => {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
doc.source,
|
|
148
|
-
doc.paragraphs.map((paragraph) => paragraph.span),
|
|
149
|
-
dates,
|
|
150
|
-
);
|
|
151
|
-
const stamps = dateStampIndexes(candidates, patternsOf(doc, "date-stamp-label"));
|
|
152
|
-
const paragraph = doc.paragraphs.find((_, index) => stamps.has(index));
|
|
153
|
-
const date = paragraph === undefined ? undefined : dates.find((candidate) => candidate.start >= paragraph.span.start && candidate.end <= paragraph.span.end);
|
|
154
|
-
return date === undefined ? undefined : yearOf(date.value);
|
|
145
|
+
const date = documentDateOf(doc, tree);
|
|
146
|
+
return date === undefined ? undefined : yearOf(date);
|
|
155
147
|
};
|
|
156
148
|
|
|
157
149
|
/** 起点の年から数えた年数が、文書の日付と合わない。 */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The document's own date: the first date of its date-stamp paragraph (a paragraph that is only a date, or 「更新日:」).
|
|
2
|
+
import type { ProseDocument, StructureNode } from "../plugin.ts";
|
|
3
|
+
import { inDocumentOrder } from "../structure/issues.ts";
|
|
4
|
+
import { dateStampIndexes, stampCandidates } from "../date-stamp.ts";
|
|
5
|
+
|
|
6
|
+
/** The date of the document's date stamp as the tree reads it ("2026-10-06", or "2026" for a year alone), when it has one. */
|
|
7
|
+
export const documentDateOf = (doc: ProseDocument, tree: StructureNode): string | undefined => {
|
|
8
|
+
const dates = inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ ...node.span, value: String(node.attrs["value"]) }] : []));
|
|
9
|
+
const candidates = stampCandidates(
|
|
10
|
+
doc.source,
|
|
11
|
+
doc.paragraphs.map((paragraph) => paragraph.span),
|
|
12
|
+
dates,
|
|
13
|
+
);
|
|
14
|
+
const labels = (doc.lexicons["date-stamp-label"] ?? []).map((entry) => entry.pattern);
|
|
15
|
+
const stamps = dateStampIndexes(candidates, labels);
|
|
16
|
+
const paragraph = doc.paragraphs.find((_, index) => stamps.has(index));
|
|
17
|
+
return paragraph === undefined ? undefined : dates.find((candidate) => candidate.start >= paragraph.span.start && candidate.end <= paragraph.span.end)?.value;
|
|
18
|
+
};
|
|
@@ -5,6 +5,7 @@ import { isKatakanaWord, stemOf } from "../long-vowel.ts";
|
|
|
5
5
|
import { QUOTATION_MARKS, isWithinAny, quotedSpans } from "../quoted-span.ts";
|
|
6
6
|
import { nameSpans, touchesAny } from "../team-names.ts";
|
|
7
7
|
import { furiganaSpans } from "../furigana.ts";
|
|
8
|
+
import { acronymsIn, isCapitalsNotSpelling } from "../capitals-with-small.ts";
|
|
8
9
|
|
|
9
10
|
/** A word found in the document: where it is, which sentence it is in, its key and spelling, and how it is written when that differs. */
|
|
10
11
|
type Placed = KeyedWord & { readonly sentence: Sentence; readonly offset: number; readonly shown?: string };
|
|
@@ -150,17 +151,20 @@ const isPartOfAddress = (text: string, start: number, end: number): boolean =>
|
|
|
150
151
|
|
|
151
152
|
/**
|
|
152
153
|
* Latin words keyed without hyphens and case (e-mail and email, GitHub and Github). A word in capitals only (TEAMS, MAY) is
|
|
153
|
-
* emphasis or an acronym, not a spelling of the word, and is left out
|
|
154
|
+
* emphasis or an acronym, not a spelling of the word, and is left out; so are a name in capitals with an ending (SENDs) and
|
|
155
|
+
* the letters of an acronym of the sentence picked out (CLImatology for CLIPER).
|
|
154
156
|
*/
|
|
155
|
-
const latinWords = (sentence: Sentence, isOpen: IsOpen, skip: ReadonlySet<string>): Placed[] =>
|
|
156
|
-
|
|
157
|
+
const latinWords = (sentence: Sentence, isOpen: IsOpen, skip: ReadonlySet<string>): Placed[] => {
|
|
158
|
+
const acronyms = acronymsIn(sentence.text);
|
|
159
|
+
return [...sentence.text.matchAll(LATIN_WORD)].flatMap((match): Placed[] => {
|
|
157
160
|
const word = match[0];
|
|
158
161
|
const plain = word.toLowerCase().replaceAll("-", "");
|
|
159
162
|
const end = match.index + word.length;
|
|
160
|
-
if (plain.length < MIN_LATIN_KEY || !LOWER.test(word) || skip.has(word.toLowerCase())) return [];
|
|
163
|
+
if (plain.length < MIN_LATIN_KEY || !LOWER.test(word) || isCapitalsNotSpelling(word, acronyms) || skip.has(word.toLowerCase())) return [];
|
|
161
164
|
if (isPartOfAddress(sentence.text, match.index, end) || !isOpen(match.index, end)) return [];
|
|
162
165
|
return [{ key: `latin|${plain}`, spelling: latinSpelling(word), shown: word, sentence, offset: sentence.span.start + match.index }];
|
|
163
166
|
});
|
|
167
|
+
};
|
|
164
168
|
|
|
165
169
|
/** How the usual spelling is written where it first appears (gitHub is shown GitHub). */
|
|
166
170
|
const shownUsual = (words: readonly Placed[], { word, usual }: OddSpelling<Placed>): string =>
|
|
@@ -4,6 +4,7 @@ import { overlapsAny, spanIndex } from "../compare/spans.ts";
|
|
|
4
4
|
import type { DurationUnit } from "../derived/date-arithmetic.ts";
|
|
5
5
|
import { relativeMismatches, type DatedValue, type Relative } from "../derived/relative-dates.ts";
|
|
6
6
|
import { quoteAt } from "./structure-tree.ts";
|
|
7
|
+
import { dayWordFindings } from "./relative-day-words.ts";
|
|
7
8
|
|
|
8
9
|
/** 期間の単位の語彙表。relative-year-unit は、向きの語が付くときだけ期間になる単位(「1年前」の年)。 */
|
|
9
10
|
const DURATION_LEXICONS: readonly (readonly [string, DurationUnit])[] = [
|
|
@@ -96,7 +97,7 @@ export const relativeDateMismatch: Detector = (doc): Finding[] => {
|
|
|
96
97
|
relatives: relativesOf(doc, amounts),
|
|
97
98
|
links: patternsOf(doc, "relative-link"),
|
|
98
99
|
};
|
|
99
|
-
|
|
100
|
+
const counted = relativeMismatches(input).map((mismatch): Finding => ({
|
|
100
101
|
rule: "relative-date-mismatch",
|
|
101
102
|
severity: "warning",
|
|
102
103
|
line: 0,
|
|
@@ -110,4 +111,5 @@ export const relativeDateMismatch: Detector = (doc): Finding[] => {
|
|
|
110
111
|
offset: mismatch.target.start,
|
|
111
112
|
},
|
|
112
113
|
}));
|
|
114
|
+
return [...counted, ...dayWordFindings(doc, dates)];
|
|
113
115
|
};
|