#41 少々しやぅ修正
このコミットが含まれているのは:
@@ -16,11 +16,12 @@ class GekanatorQuestionsController < ApplicationController
|
||||
private
|
||||
|
||||
def question_json question
|
||||
condition = condition_json(question.condition).deep_symbolize_keys
|
||||
json = {
|
||||
id: question_id_for(question),
|
||||
text: question.text,
|
||||
id: question_id_for(question, condition),
|
||||
text: question_text_for(question, condition),
|
||||
kind: question.kind,
|
||||
condition: condition_json(question.condition),
|
||||
condition: condition,
|
||||
source: question.source,
|
||||
priority_weight: question.priority_weight
|
||||
}
|
||||
@@ -30,9 +31,7 @@ class GekanatorQuestionsController < ApplicationController
|
||||
json
|
||||
end
|
||||
|
||||
def question_id_for question
|
||||
condition = condition_json(question.condition).deep_symbolize_keys
|
||||
|
||||
def question_id_for question, condition
|
||||
case condition[:type]
|
||||
when 'tag'
|
||||
"tag:#{ condition[:key] }"
|
||||
@@ -44,8 +43,10 @@ class GekanatorQuestionsController < ApplicationController
|
||||
"original-month:#{ condition[:month] }"
|
||||
when 'original-month-day'
|
||||
"original-month-day:#{ condition[:monthDay] || condition[:month_day] }"
|
||||
when 'title-length-at-least'
|
||||
"title:length-at-least:#{ condition[:length] }"
|
||||
when 'title-length-greater-than'
|
||||
"title:length-greater-than:#{ condition[:length] }"
|
||||
"title:length-at-least:#{ condition[:length].to_i + 1 }"
|
||||
when 'title-has-ascii'
|
||||
'title:ascii'
|
||||
when 'post-similarity'
|
||||
@@ -62,9 +63,25 @@ class GekanatorQuestionsController < ApplicationController
|
||||
json['monthDay'] = json.delete('month_day')
|
||||
end
|
||||
|
||||
if json['type'] == 'title-length-greater-than'
|
||||
json['type'] = 'title-length-at-least'
|
||||
json['length'] = json['length'].to_i + 1
|
||||
end
|
||||
|
||||
json
|
||||
end
|
||||
|
||||
def question_text_for question, condition
|
||||
return question.text unless question.kind == 'title'
|
||||
|
||||
case condition[:type]
|
||||
when 'title-length-at-least'
|
||||
"タイトルは #{ condition[:length] } 文字以上?"
|
||||
else
|
||||
question.text
|
||||
end
|
||||
end
|
||||
|
||||
def example_answers_json question
|
||||
question
|
||||
.gekanator_question_examples
|
||||
|
||||
@@ -35,6 +35,7 @@ export type GekanatorQuestionCondition =
|
||||
| { type: 'original-year'; year: number }
|
||||
| { type: 'original-month'; month: number }
|
||||
| { type: 'original-month-day'; monthDay: string }
|
||||
| { type: 'title-length-at-least'; length: number }
|
||||
| { type: 'title-length-greater-than'; length: number }
|
||||
| { type: 'title-has-ascii' }
|
||||
| {
|
||||
@@ -44,6 +45,12 @@ export type GekanatorQuestionCondition =
|
||||
threshold: number
|
||||
}
|
||||
|
||||
|
||||
type NonPostSimilarityCondition = Exclude<
|
||||
GekanatorQuestionCondition,
|
||||
{ type: 'post-similarity' }
|
||||
>
|
||||
|
||||
export type GekanatorExtraQuestion = {
|
||||
id: number
|
||||
text: string
|
||||
@@ -70,10 +77,67 @@ export type GekanatorQuestion = {
|
||||
test: (post: Post) => boolean }
|
||||
|
||||
|
||||
export const normalizeTitleLengthCondition = (
|
||||
condition: GekanatorQuestionCondition,
|
||||
): GekanatorQuestionCondition => {
|
||||
switch (condition.type)
|
||||
{
|
||||
case 'title-length-greater-than':
|
||||
return {
|
||||
type: 'title-length-at-least',
|
||||
length: condition.length + 1 }
|
||||
default:
|
||||
return condition
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const titleLengthMinimumForCondition = (
|
||||
condition: GekanatorQuestionCondition,
|
||||
): number | null => {
|
||||
switch (condition.type)
|
||||
{
|
||||
case 'title-length-at-least':
|
||||
return condition.length
|
||||
case 'title-length-greater-than':
|
||||
return condition.length + 1
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const questionIdForCondition = (
|
||||
condition: NonPostSimilarityCondition,
|
||||
): string => {
|
||||
switch (condition.type)
|
||||
{
|
||||
case 'tag':
|
||||
return `tag:${ condition.key }`
|
||||
case 'source':
|
||||
return `source:${ condition.host }`
|
||||
case 'original-year':
|
||||
return `original-year:${ condition.year }`
|
||||
case 'original-month':
|
||||
return `original-month:${ condition.month }`
|
||||
case 'original-month-day':
|
||||
return `original-month-day:${ condition.monthDay }`
|
||||
case 'title-length-at-least':
|
||||
case 'title-length-greater-than':
|
||||
return `title:length-at-least:${ titleLengthMinimumForCondition (condition) }`
|
||||
case 'title-has-ascii':
|
||||
return 'title:ascii'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const directExampleAnswerFor = (
|
||||
question: StoredGekanatorQuestion,
|
||||
post: Post,
|
||||
): GekanatorAnswerValue | null => {
|
||||
if (question.kind !== 'post_similarity')
|
||||
return null
|
||||
|
||||
const direct = question.exampleAnswers?.[String (post.id) as `${ number }`]
|
||||
if (direct)
|
||||
return direct
|
||||
@@ -222,6 +286,8 @@ const questionMatches = (
|
||||
return originalMonthOf (post) === question.condition.month
|
||||
case 'original-month-day':
|
||||
return originalMonthDayOf (post) === question.condition.monthDay
|
||||
case 'title-length-at-least':
|
||||
return (post.title?.length ?? 0) >= question.condition.length
|
||||
case 'title-length-greater-than':
|
||||
return (post.title?.length ?? 0) > question.condition.length
|
||||
case 'title-has-ascii':
|
||||
@@ -250,6 +316,7 @@ export const expectedAnswerForQuestion = (
|
||||
case 'original-year':
|
||||
case 'original-month':
|
||||
case 'original-month-day':
|
||||
case 'title-length-at-least':
|
||||
case 'title-length-greater-than':
|
||||
case 'title-has-ascii':
|
||||
return questionMatches (post, question) ? 'yes' : 'no'
|
||||
@@ -261,20 +328,32 @@ export const expectedAnswerForQuestion = (
|
||||
|
||||
export const restoreGekanatorQuestion = (
|
||||
question: StoredGekanatorQuestion,
|
||||
): GekanatorQuestion => ({
|
||||
): GekanatorQuestion => {
|
||||
const normalizedCondition = normalizeTitleLengthCondition (question.condition)
|
||||
const normalizedQuestion = {
|
||||
...question,
|
||||
id: normalizedCondition.type === 'title-length-at-least'
|
||||
? `title:length-at-least:${ normalizedCondition.length }`
|
||||
: question.id,
|
||||
condition: normalizedCondition,
|
||||
source: question.source ?? 'default',
|
||||
priorityWeight: question.priorityWeight ?? 1,
|
||||
test: (post: Post) => questionMatches (post, question) })
|
||||
priorityWeight: question.priorityWeight ?? 1 }
|
||||
|
||||
return {
|
||||
...normalizedQuestion,
|
||||
test: (post: Post) => questionMatches (post, normalizedQuestion) }
|
||||
}
|
||||
|
||||
|
||||
export const storeGekanatorQuestion = (
|
||||
question: GekanatorQuestion,
|
||||
): StoredGekanatorQuestion => ({
|
||||
id: question.id,
|
||||
id: question.condition.type === 'title-length-greater-than'
|
||||
? `title:length-at-least:${ question.condition.length + 1 }`
|
||||
: question.id,
|
||||
text: question.text,
|
||||
kind: question.kind,
|
||||
condition: question.condition,
|
||||
condition: normalizeTitleLengthCondition (question.condition),
|
||||
source: question.source,
|
||||
priorityWeight: question.priorityWeight,
|
||||
exampleAnswers: question.exampleAnswers })
|
||||
@@ -402,15 +481,15 @@ export const buildGekanatorQuestions = (posts: Post[]): GekanatorQuestion[] => {
|
||||
|
||||
const titleQuestions = [
|
||||
{
|
||||
id: 'title:long',
|
||||
text: '題名が長めの投稿?',
|
||||
id: `title:length-at-least:${ titleLengthMedian }`,
|
||||
text: `タイトルは ${ titleLengthMedian } 文字以上?`,
|
||||
kind: 'title' as const,
|
||||
condition: {
|
||||
type: 'title-length-greater-than' as const,
|
||||
type: 'title-length-at-least' as const,
|
||||
length: titleLengthMedian },
|
||||
source: 'default' as const,
|
||||
priorityWeight: 1,
|
||||
test: (post: Post) => (post.title?.length ?? 0) > titleLengthMedian },
|
||||
test: (post: Post) => (post.title?.length ?? 0) >= titleLengthMedian },
|
||||
{
|
||||
id: 'title:ascii',
|
||||
text: '題名に英数字が混じってゐる?',
|
||||
|
||||
@@ -10,11 +10,13 @@ import { buildGekanatorQuestions,
|
||||
fetchGekanatorExtraQuestions,
|
||||
fetchGekanatorQuestions,
|
||||
fetchGekanatorPosts,
|
||||
normalizeTitleLengthCondition,
|
||||
restoreGekanatorQuestion,
|
||||
saveGekanatorExtraQuestionAnswers,
|
||||
saveGekanatorGame,
|
||||
saveGekanatorQuestionSuggestion,
|
||||
storeGekanatorQuestion } from '@/lib/gekanator'
|
||||
storeGekanatorQuestion,
|
||||
titleLengthMinimumForCondition } from '@/lib/gekanator'
|
||||
import { gekanatorKeys } from '@/lib/queryKeys'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -23,6 +25,7 @@ import type { FC } from 'react'
|
||||
import type { GekanatorAnswerLog,
|
||||
GekanatorAnswerValue,
|
||||
GekanatorExtraQuestion,
|
||||
GekanatorQuestionCondition,
|
||||
GekanatorQuestion,
|
||||
StoredGekanatorQuestion } from '@/lib/gekanator'
|
||||
import type { Post } from '@/types'
|
||||
@@ -144,6 +147,46 @@ const createGameSeed = (): string => {
|
||||
}
|
||||
|
||||
|
||||
const normalizeStoredQuestionId = (
|
||||
questionId: string,
|
||||
condition?: GekanatorQuestionCondition,
|
||||
): string => {
|
||||
if (condition?.type === 'title-length-greater-than')
|
||||
return `title:length-at-least:${ condition.length + 1 }`
|
||||
|
||||
if (questionId.startsWith ('title:length-greater-than:'))
|
||||
{
|
||||
const length = Number (questionId.split (':').pop ())
|
||||
if (Number.isInteger (length))
|
||||
return `title:length-at-least:${ length + 1 }`
|
||||
}
|
||||
|
||||
return questionId
|
||||
}
|
||||
|
||||
|
||||
const normalizeStoredGame = (game: StoredGekanatorGame): StoredGekanatorGame => ({
|
||||
...game,
|
||||
answers: game.answers.map (answer => ({
|
||||
...answer,
|
||||
questionId: normalizeStoredQuestionId (
|
||||
answer.questionId,
|
||||
answer.questionCondition),
|
||||
questionCondition: answer.questionCondition
|
||||
? normalizeTitleLengthCondition (answer.questionCondition)
|
||||
: undefined })),
|
||||
askedIds: game.askedIds.map (questionId => normalizeStoredQuestionId (questionId)),
|
||||
softenedQuestionIds: game.softenedQuestionIds.map (questionId =>
|
||||
normalizeStoredQuestionId (questionId)),
|
||||
askedQuestionBank: game.askedQuestionBank?.map (question =>
|
||||
({
|
||||
...question,
|
||||
id: normalizeStoredQuestionId (question.id, question.condition),
|
||||
condition: normalizeTitleLengthCondition (question.condition) })),
|
||||
askedQuestionBankIds: game.askedQuestionBankIds?.map (questionId =>
|
||||
normalizeStoredQuestionId (questionId)) })
|
||||
|
||||
|
||||
const sourcePriorityForMerge = (question: GekanatorQuestion): number => {
|
||||
switch (question.source)
|
||||
{
|
||||
@@ -214,7 +257,7 @@ const loadStoredGame = (): StoredGekanatorGame | null => {
|
||||
if (!(raw))
|
||||
return null
|
||||
|
||||
return JSON.parse (raw) as StoredGekanatorGame
|
||||
return normalizeStoredGame (JSON.parse (raw) as StoredGekanatorGame)
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -548,6 +591,13 @@ const sameConditionValue = (
|
||||
left: GekanatorQuestion['condition'],
|
||||
right: GekanatorQuestion['condition'],
|
||||
): boolean => {
|
||||
const leftTitleLength = titleLengthMinimumForCondition (left)
|
||||
const rightTitleLength = titleLengthMinimumForCondition (right)
|
||||
if (leftTitleLength !== null || rightTitleLength !== null)
|
||||
return leftTitleLength !== null
|
||||
&& rightTitleLength !== null
|
||||
&& leftTitleLength === rightTitleLength
|
||||
|
||||
if (left.type !== right.type)
|
||||
return false
|
||||
|
||||
@@ -564,12 +614,13 @@ const sameConditionValue = (
|
||||
return String (condition.month)
|
||||
case 'original-month-day':
|
||||
return condition.monthDay
|
||||
case 'title-length-greater-than':
|
||||
return String (condition.length)
|
||||
case 'title-has-ascii':
|
||||
return ''
|
||||
case 'post-similarity':
|
||||
return `${ condition.postId }:${ condition.answer }:${ condition.threshold }`
|
||||
case 'title-length-at-least':
|
||||
case 'title-length-greater-than':
|
||||
return String (titleLengthMinimumForCondition (condition) ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,6 +642,38 @@ const monthForCondition = (
|
||||
}
|
||||
|
||||
|
||||
const isTitleLengthContradiction = (
|
||||
candidate: GekanatorQuestion['condition'],
|
||||
previous: GekanatorQuestion['condition'],
|
||||
answer: GekanatorAnswerValue,
|
||||
): boolean => {
|
||||
const candidateLength = titleLengthMinimumForCondition (candidate)
|
||||
const previousLength = titleLengthMinimumForCondition (previous)
|
||||
if (candidateLength === null || previousLength === null)
|
||||
return false
|
||||
|
||||
switch (answer)
|
||||
{
|
||||
case 'yes':
|
||||
return candidateLength <= previousLength
|
||||
case 'no':
|
||||
return candidateLength >= previousLength
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const isQuestionRedundantAfterAnswers = (
|
||||
question: GekanatorQuestion,
|
||||
answers: GekanatorAnswerLog[],
|
||||
): boolean => answers.some (answer => {
|
||||
const previous = answer.questionCondition
|
||||
return previous !== undefined
|
||||
&& isTitleLengthContradiction (question.condition, previous, answer.answer)
|
||||
})
|
||||
|
||||
|
||||
const isMonthCrossMatch = (
|
||||
candidate: GekanatorQuestion['condition'],
|
||||
previous: GekanatorQuestion['condition'],
|
||||
@@ -725,6 +808,9 @@ const chooseQuestion = ({
|
||||
|
||||
return questionsToRank
|
||||
.map (question => {
|
||||
if (isQuestionRedundantAfterAnswers (question, answers))
|
||||
return null
|
||||
|
||||
const signature = signatureFor (question, candidates)
|
||||
if (redundant.has (signature))
|
||||
return null
|
||||
|
||||
新しい課題から参照
ユーザをブロックする