feat(sentiment): Improve scoring and analysis

* Implement english-only scoring with wink https://github.com/winkjs/wink-sentiment (AFINN, emojis)
* Implement english-only scoring with NLP.js https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md (AFINN, Senticon, Pattern)
* Refactor language processing into standalone functions for future use
* Add limited multi-langauge support
  * Can run sentiment with NLP.js on english, german, spanish, and french
* Normalize all scores to range between -1 and +1
* Improve score accuracy by averaging all scores
This commit is contained in:
FoxxMD
2022-05-11 16:09:29 -04:00
parent c919532aac
commit 4411d1a413
10 changed files with 2208 additions and 183 deletions

1865
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,7 @@
"mustache": "^4.2.0",
"nanoid": "^3.3.1",
"node-fetch": "^2.6.1",
"node-nlp": "^4.24.0",
"normalize-url": "^6.1.0",
"object-hash": "^2.2.0",
"p-event": "^4.2.0",
@@ -92,6 +93,7 @@
"typescript": "^4.3.4",
"vader-sentiment": "^1.1.3",
"webhook-discord": "^3.7.7",
"wink-sentiment": "^5.0.2",
"winston": "github:FoxxMD/winston#fbab8de969ecee578981c77846156c7f43b5f01e",
"winston-daily-rotate-file": "^4.5.5",
"winston-duplex": "^0.1.1",

View File

@@ -190,29 +190,40 @@ export type ActionTypes =
/**
* Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language
*
* Sentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:
* Sentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:
*
* * -0.5 => extremely negative
* * -0.2 => very negative
* * -0.5 => negative
* * -0.6 => extremely negative
* * -0.3 => very negative
* * -0.1 => negative
* * 0 => neutral
* * 0.05 => positive
* * 0.2 => very positive
* * 0.5 => extremely positive
* * 0.1 => positive
* * 0.3 => very positive
* * 0.6 => extremely positive
*
* The below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)
*
* * `>= 0.05` = `is positive`
* * `<= 0.2` = `is very negative`
* * `< 0.05` = `is not positive`
* * `> -0.2` = `is not very negative`
* * `>= 0.1` = `is positive`
* * `<= 0.3` = `is very negative`
* * `< 0.1` = `is not positive`
* * `> -0.3` = `is not very negative`
*
* Special case:
*
* * `is neutral` equates to `> -0.5 and < 0.5`
* * `is not neutral` equates to `< -0.5 or > 0.5`
* * `is neutral` equates to `> -0.1 and < 0.1`
* * `is not neutral` equates to `< -0.1 or > 0.1`
*
* More about VADER Sentiment: https://github.com/cjhutto/vaderSentiment
* ContextMod uses a normalized, weighted average from these sentiment tools:
*
* * NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md
* * (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/
* * (english only) wink-sentiment https://github.com/winkjs/wink-sentiment
*
* More about the sentiment algorithms used:
* * VADER https://github.com/cjhutto/vaderSentiment
* * AFINN http://corpustext.com/reference/sentiment_afinn.html
* * Senticon https://ieeexplore.ieee.org/document/8721408
* * Pattern https://github.com/clips/pattern
* * wink https://github.com/winkjs/wink-sentiment
*
* @pattern ((>|>=|<|<=)\s*(-?\d?\.?\d+))|((not)?\s*(very|extremely)?\s*(positive|neutral|negative))
* @examples ["is negative", "> 0.2"]

View File

@@ -0,0 +1,267 @@
import {Language, SentimentManager} from 'node-nlp';
import {SentimentIntensityAnalyzer} from 'vader-sentiment';
import wink from 'wink-sentiment';
import {SnoowrapActivity} from "./Infrastructure/Reddit";
import {
asGenericComparison,
GenericComparison,
parseGenericValueComparison,
RangedComparison
} from "./Infrastructure/Comparisons";
import {asSubmission, between, comparisonTextOp} from "../util";
import {CMError, MaybeSeriousErrorWithCause} from "../Utils/Errors";
import InvalidRegexError from "../Utils/InvalidRegexError";
import {StringOperator} from "./Infrastructure/Atomic";
export type SentimentAnalysisType = 'vader' | 'afinn' | 'senticon' | 'pattern' | 'wink';
export const sentimentQuantifier = {
'extremely negative': -0.6,
'very negative': -0.3,
'negative': -0.1,
'neutral': 0,
'positive': 0.1,
'very positive': 0.3,
'extremely positive': 0.6,
}
export const sentimentQuantifierRanges = [
{
range: [Number.MIN_SAFE_INTEGER, -0.6],
quant: 'extremely negative'
},
{
range: [-0.6, -0.3],
quant: 'very negative'
},
{
range: [-0.3, -0.1],
quant: 'negative'
},
{
range: [-0.1, 0.1],
quant: 'neutral'
},
{
range: [0.1, 0.3],
quant: 'positive'
},
{
range: [0.3, 0.6],
quant: 'very positive'
},
{
range: [0.6, Number.MAX_SAFE_INTEGER],
quant: 'extremely positive'
}
]
const scoreToSentimentText = (val: number) => {
for(const segment of sentimentQuantifierRanges) {
if(between(val, segment.range[0], segment.range[1], false)) {
return segment.quant;
}
}
throw new Error('should not hit this!');
}
export interface SentimentResult {
comparative: number
type: SentimentAnalysisType
sentiment: string
weight: number
}
export interface ActivitySentiment {
results: SentimentResult[]
score: number
scoreWeighted: number
sentiment: string
sentimentWeighted: string
activity: SnoowrapActivity
language: string
}
export interface ActivitySentimentTestResult extends ActivitySentiment {
passes: boolean
test: GenericComparison | RangedComparison
}
export interface ActivitySentimentOptions {
testOn?: ('title' | 'body')[]
}
export type SentimentCriteriaTest = GenericComparison | RangedComparison;
export const availableSentimentLanguages = ['en', 'es', 'de', 'fr'];
export const textComparison = /(?<not>not)?\s*(?<modifier>very|extremely)?\s*(?<sentiment>positive|neutral|negative)/i;
export const parseTextToNumberComparison = (val: string): RangedComparison | GenericComparison => {
let genericError: Error | undefined;
try {
return parseGenericValueComparison(val);
} catch (e) {
genericError = e as Error;
// now try text match
}
const matches = val.match(textComparison);
if (matches === null) {
const textError = new InvalidRegexError(textComparison, val);
throw new CMError(`Sentiment value did not match a valid numeric comparison or valid text: \n ${genericError.message} \n ${textError.message}`);
}
const groups = matches.groups as any;
const negate = groups.not !== undefined && groups.not !== '';
if(groups.sentiment === 'neutral') {
if(negate) {
return {
displayText: 'not neutral (not -0.49 to 0.49)',
range: [-1, 1],
not: true,
}
}
return {
displayText: 'is neutral (-0.49 to 0.49)',
range: [-1, 1],
not: false
}
}
const compoundSentimentText = `${groups.modifier !== undefined && groups.modifier !== '' ? `${groups.modifier} ` : ''}${groups.sentiment}`.toLocaleLowerCase();
// @ts-ignore
const numericVal = sentimentQuantifier[compoundSentimentText] as number;
if(numericVal === undefined) {
throw new CMError(`Sentiment given did not match any known phrases: '${compoundSentimentText}'`);
}
let operator: StringOperator;
if(negate) {
operator = numericVal > 0 ? '<' : '>';
} else {
operator = numericVal > 0 ? '>=' : '<=';
}
return {
operator,
value: numericVal,
isPercent: false,
displayText: `is${negate ? ' not ': ' '}${compoundSentimentText} (${operator} ${numericVal})`
}
}
const nlpAnalyzer = new SentimentManager();
const langDetect = new Language();
export const getActivitySentiment = async (item: SnoowrapActivity, options?: ActivitySentimentOptions): Promise<ActivitySentiment> => {
const {
testOn = ['body', 'title']
} = options || {};
// determine what content we are testing
let contents: string[] = [];
if (asSubmission(item)) {
for (const l of testOn) {
switch (l) {
case 'title':
contents.push(item.title);
break;
case 'body':
if (item.is_self) {
contents.push(item.selftext);
}
break;
}
}
} else {
contents.push(item.body)
}
const contentStr = contents.join(' ');
const guess = langDetect.guessBest(contentStr);
if (availableSentimentLanguages.includes(guess.alpha2)) {
const results: SentimentResult[] = [];
const nlpResult = await nlpAnalyzer.process(guess.alpha2, contentStr);
results.push({
comparative: nlpResult.comparative,
type: nlpResult.type as SentimentAnalysisType,
sentiment: scoreToSentimentText(nlpResult.comparative),
weight: 1
});
if (guess.alpha2 === 'en') {
const score = SentimentIntensityAnalyzer.polarity_scores(contentStr);
results.push({
comparative: score.compound,
type: 'vader',
sentiment: scoreToSentimentText(score.compound),
// may want to weight higher in the future...
weight: 1
});
const winkScore = wink(contentStr);
// normalizedScore is range of -5 to +5 -- convert to -1 to +1
const winkAdjusted = (winkScore.normalizedScore * 2) / 10;
results.push({
comparative: winkAdjusted,
type: 'wink',
sentiment: scoreToSentimentText(winkAdjusted),
weight: 1
})
}
const score = results.reduce((acc, curr) => acc + curr.comparative, 0) / results.length;
const sentiment = scoreToSentimentText(score);
const weightSum = results.reduce((acc, curr) => acc + curr.weight, 0);
const weightedScores = results.reduce((acc, curr) => acc + (curr.weight * curr.comparative), 0);
const weightedScore = weightedScores / weightSum;
const weightedSentiment = scoreToSentimentText(weightedScore);
return {
results,
score,
sentiment,
scoreWeighted: weightedScore,
sentimentWeighted: weightedSentiment,
activity: item,
language: guess.language
}
} else {
throw new MaybeSeriousErrorWithCause(`Cannot test sentiment for unsupported language ${guess.language}`);
}
}
export const testActivitySentiment = async (item: SnoowrapActivity, criteria: SentimentCriteriaTest, options?: ActivitySentimentOptions): Promise<ActivitySentimentTestResult> => {
const sentimentResult = await getActivitySentiment(item, options);
if (asGenericComparison(criteria)) {
return {
passes: comparisonTextOp(sentimentResult.scoreWeighted, criteria.operator, criteria.value),
test: criteria,
...sentimentResult,
}
} else {
if (criteria.not) {
return {
passes: sentimentResult.scoreWeighted < criteria.range[0] || sentimentResult.scoreWeighted > criteria.range[1],
test: criteria,
...sentimentResult,
}
}
return {
passes: sentimentResult.scoreWeighted >= criteria.range[0] || sentimentResult.scoreWeighted <= criteria.range[1],
test: criteria,
...sentimentResult,
}
}
}

View File

@@ -32,3 +32,23 @@ declare module 'winston-null' {
}
}
declare module 'node-nlp' {
export class Language {
guess(val: string, allowedList?: string[] | null, limit: number): {alpha3: string, alpha2: string, language: string, score: number}[];
guessBest(val: string, allowedList?: string[] | null): {alpha3: string, alpha2: string, language: string, score: number};
}
export class SentimentAnalyzer {
constructor(settings?: {language?: string})
getSentiment(phrase: string): Promise<{score: number, comparative: score, vote: 'string', numWords: number, numHits: number, type: string, language: string}>
}
export class SentimentManager {
process(locale: string, phrase: string): Promise<{score: number, comparative: score, vote: 'string', numWords: number, numHits: number, type: string, language: string}>
}
}
declare module 'wink-sentiment' {
function sentiment(phrase: string): {score: number, normalizedScore: number, tokenizedPhrase: any[]};
export default sentiment;
}

View File

@@ -2,123 +2,26 @@ import {Rule, RuleJSONConfig, RuleOptions} from "./index";
import {Comment} from "snoowrap";
import Submission from "snoowrap/dist/objects/Submission";
import {
asSubmission,
comparisonTextOp, formatNumber,
parseStringToRegex,
triggeredIndicator, windowConfigToWindowCriteria
} from "../util";
import {Scores, SentimentIntensityAnalyzer} from 'vader-sentiment';
import dayjs from 'dayjs';
import {CMError, SimpleError} from "../Utils/Errors";
import InvalidRegexError from "../Utils/InvalidRegexError";
import {map as mapAsync} from 'async';
import {
asGenericComparison,
GenericComparison,
parseGenericValueComparison, parseGenericValueOrPercentComparison,
parseGenericValueOrPercentComparison,
RangedComparison
} from "../Common/Infrastructure/Comparisons";
import {ActivityWindowConfig, ActivityWindowCriteria} from "../Common/Infrastructure/ActivityWindow";
import {StringOperator, VaderSentimentComparison} from "../Common/Infrastructure/Atomic";
import {VaderSentimentComparison} from "../Common/Infrastructure/Atomic";
import {RuleResult} from "../Common/interfaces";
import {SnoowrapActivity} from "../Common/Infrastructure/Reddit";
export const sentimentQuantifier = {
'extremely negative': -0.5,
'very negative': -0.2,
'negative': -0.05,
'neutral': 0,
'positive': 0.05,
'very positive': 0.2,
'extremely positive': 0.5,
}
const sentimentQuantifierEntries = Object.entries(sentimentQuantifier);
const scoreToSentimentText = (val: number) => {
for(let i = 0; i < sentimentQuantifierEntries.length; i++) {
// hit max score (extremely positive)
if(i + 1 === sentimentQuantifierEntries.length && val > sentimentQuantifierEntries[i][1]) {
return sentimentQuantifierEntries[i][0];
}
// if score is between (inclusive start) current and next
if(i + 1 <= sentimentQuantifierEntries.length && val >= sentimentQuantifierEntries[i][1] && val < sentimentQuantifierEntries[i + 1][1]) {
return sentimentQuantifierEntries[i][0];
}
// if neither of those is hit and i = 0 then min score (extremely negative)
if(i === 0 && val < sentimentQuantifierEntries[i][1]) {
return sentimentQuantifierEntries[i][0];
}
}
throw new Error('should not hit this!');
}
interface ActivitySentimentResult {
passes: boolean,
scores: Scores,
sentiment: string
activity: SnoowrapActivity
}
export const textComparison = /(?<not>not)?\s*(?<modifier>very|extremely)?\s*(?<sentiment>positive|neutral|negative)/i;
export const parseTextToNumberComparison = (val: string): RangedComparison | GenericComparison => {
let genericError: Error | undefined;
try {
return parseGenericValueComparison(val);
} catch (e) {
genericError = e as Error;
// now try text match
}
const matches = val.match(textComparison);
if (matches === null) {
const textError = new InvalidRegexError(textComparison, val);
throw new CMError(`Sentiment value did not match a valid numeric comparison or valid text: \n ${genericError.message} \n ${textError.message}`);
}
const groups = matches.groups as any;
const negate = groups.not !== undefined && groups.not !== '';
if(groups.sentiment === 'neutral') {
if(negate) {
return {
displayText: 'not neutral (not -0.49 to 0.49)',
range: [-0.49, 0.49],
not: true,
}
}
return {
displayText: 'is neutral (-0.49 to 0.49)',
range: [-0.49, 0.49],
not: false
}
}
const compoundSentimentText = `${groups.modifier !== undefined && groups.modifier !== '' ? `${groups.modifier} ` : ''}${groups.sentiment}`.toLocaleLowerCase();
// @ts-ignore
const numericVal = sentimentQuantifier[compoundSentimentText] as number;
if(numericVal === undefined) {
throw new CMError(`Sentiment given did not match any known phrases: '${compoundSentimentText}'`);
}
let operator: StringOperator;
if(negate) {
operator = numericVal > 0 ? '<' : '>';
} else {
operator = numericVal > 0 ? '>=' : '<=';
}
return {
operator,
value: numericVal,
isPercent: false,
displayText: `is${negate ? ' not ': ' '}${compoundSentimentText} (${operator} ${numericVal})`
}
}
import {
ActivitySentimentTestResult,
parseTextToNumberComparison,
testActivitySentiment
} from "../Common/LangaugeProcessing";
export class SentimentRule extends Rule {
@@ -166,10 +69,8 @@ export class SentimentRule extends Rule {
protected async process(item: Submission | Comment): Promise<[boolean, RuleResult]> {
let criteriaResults = [];
let ogResult = this.testActivity(item, this.sentiment);
let historicResults: ActivitySentimentResult[] | undefined;
let ogResult = await this.testActivity(item, this.sentiment);
let historicResults: ActivitySentimentTestResult[] | undefined;
if(this.historical !== undefined && (!this.historical.mustMatchCurrent || ogResult.passes)) {
const {
@@ -178,7 +79,7 @@ export class SentimentRule extends Rule {
} = this.historical;
const history = await this.resources.getAuthorActivities(item.author, window);
historicResults = history.map(x => this.testActivity(x, sentiment));
historicResults = await mapAsync(history, async (x: SnoowrapActivity) => await this.testActivity(x, sentiment)); // history.map(x => this.testActivity(x, sentiment));
}
@@ -197,8 +98,8 @@ export class SentimentRule extends Rule {
if(historicResults === undefined) {
triggered = ogResult.passes;
averageScore = ogResult.scores.compound;
logSummary.push(`${triggeredIndicator(triggered)} Current Activity Sentiment '${ogResult.sentiment} (${ogResult.scores.compound})' ${triggered ? 'PASSED' : 'DID NOT PASS'} sentiment test '${sentimentTest}'`);
averageScore = ogResult.scoreWeighted;
logSummary.push(`${triggeredIndicator(triggered)} Current Activity Sentiment '${ogResult.sentiment} (${ogResult.scoreWeighted})' ${triggered ? 'PASSED' : 'DID NOT PASS'} sentiment test '${sentimentTest}'`);
if(!triggered && this.historical !== undefined && this.historical.mustMatchCurrent) {
logSummary.push(`Did not check Historical because 'mustMatchCurrent' is true`);
}
@@ -212,8 +113,8 @@ export class SentimentRule extends Rule {
totalMatchingText = totalMatching.displayText;
const allResults = historicResults
const passed = allResults.filter(x => x.passes);
averageScore = passed.reduce((acc, curr) => acc + curr.scores.compound,0) / passed.length;
averageWindowScore = allResults.reduce((acc, curr) => acc + curr.scores.compound,0) / allResults.length;
averageScore = passed.reduce((acc, curr) => acc + curr.scoreWeighted,0) / passed.length;
averageWindowScore = allResults.reduce((acc, curr) => acc + curr.scoreWeighted,0) / allResults.length;
const firstActivity = allResults[0].activity;
const lastActivity = allResults[allResults.length - 1].activity;
@@ -253,53 +154,8 @@ export class SentimentRule extends Rule {
})]);
}
protected testActivity(a: (Submission | Comment), criteria: GenericComparison | RangedComparison): ActivitySentimentResult {
// determine what content we are testing
let contents: string[] = [];
if (asSubmission(a)) {
for (const l of this.testOn) {
switch (l) {
case 'title':
contents.push(a.title);
break;
case 'body':
if (a.is_self) {
contents.push(a.selftext);
}
break;
}
}
} else {
contents.push(a.body)
}
const contentStr = contents.join(' ');
const scores = SentimentIntensityAnalyzer.polarity_scores(contentStr);
const textSentimentScore = scoreToSentimentText(scores.compound);
if (asGenericComparison(criteria)) {
return {
passes: comparisonTextOp(scores.compound, criteria.operator, criteria.value),
scores,
sentiment: textSentimentScore,
activity: a
}
} else {
if (criteria.not) {
return {
passes: scores.compound < criteria.range[0] || scores.compound > criteria.range[1],
scores,
sentiment: textSentimentScore,
activity: a
}
}
return {
passes: scores.compound >= criteria.range[0] || scores.compound <= criteria.range[1],
scores,
sentiment: textSentimentScore,
activity: a
}
}
protected async testActivity(a: (Submission | Comment), criteria: GenericComparison | RangedComparison): Promise<ActivitySentimentTestResult> {
return await testActivitySentiment(a, criteria, {testOn: this.testOn});
}
}

View File

@@ -2926,7 +2926,7 @@
"type": "boolean"
},
"sentiment": {
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
"examples": [
"is negative",
"> 0.2"
@@ -5244,7 +5244,7 @@
"type": "string"
},
"sentiment": {
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
"examples": [
"is negative",
"> 0.2"

View File

@@ -1309,7 +1309,7 @@
"type": "boolean"
},
"sentiment": {
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
"examples": [
"is negative",
"> 0.2"
@@ -2855,7 +2855,7 @@
"type": "string"
},
"sentiment": {
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
"examples": [
"is negative",
"> 0.2"

View File

@@ -1280,7 +1280,7 @@
"type": "boolean"
},
"sentiment": {
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
"examples": [
"is negative",
"> 0.2"
@@ -2826,7 +2826,7 @@
"type": "string"
},
"sentiment": {
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
"examples": [
"is negative",
"> 0.2"

View File

@@ -2757,3 +2757,11 @@ export const filterByTimeRequirement = (satisfiedEndtime: Dayjs, listSlice: Snoo
return [truncatedItems.length !== listSlice.length, truncatedItems]
}
export const between = (val: number, a: number, b: number, inclusive: boolean): boolean => {
var min = Math.min(a, b),
max = Math.max(a, b);
return inclusive ? val >= min && val <= max : val > min && val < max;
}