#41 少々しやぅ修正

このコミットが含まれているのは:
2026-06-11 23:18:23 +09:00
コミット 4caea6213a
3個のファイルの変更204行の追加22行の削除
+24 -7
ファイルの表示
@@ -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
+88 -9
ファイルの表示
@@ -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: '題名に英数字が混じってゐる?',
+90 -4
ファイルの表示
@@ -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