diff --git a/src/Entity/algorithm.ts b/src/Entity/algorithm.ts index 3ac116d..4fc40a2 100644 --- a/src/Entity/algorithm.ts +++ b/src/Entity/algorithm.ts @@ -35,8 +35,8 @@ export class Algorithm { @Column() solvedCount: number; - @Column() - point: number; + @Column('double') + score: number; @CreateDateColumn({ type: 'timestamp', diff --git a/src/Entity/github.ts b/src/Entity/github.ts index ae5d7b4..eaf863e 100644 --- a/src/Entity/github.ts +++ b/src/Entity/github.ts @@ -23,8 +23,8 @@ export class Github { @Column() githubId: number; - @Column() - point: number; + @Column('double') + score: number; @Column() accessToken: string; diff --git a/src/Entity/grade.ts b/src/Entity/grade.ts index 3202b30..d6c7712 100644 --- a/src/Entity/grade.ts +++ b/src/Entity/grade.ts @@ -23,8 +23,8 @@ export class Grade { @Column('double') grade: number; - @Column() - point: number; + @Column('double') + score: number; @CreateDateColumn({ type: 'timestamp', diff --git a/src/Entity/totalPoint.ts b/src/Entity/totalPoint.ts index 21b260f..d32d1b1 100644 --- a/src/Entity/totalPoint.ts +++ b/src/Entity/totalPoint.ts @@ -20,8 +20,8 @@ export class TotalPoint { }) userId: string; - @Column() - point: number; + @Column('double') + score: number; @CreateDateColumn({ type: 'timestamp', diff --git a/src/batch/batch.service.ts b/src/batch/batch.service.ts index 3640b61..7e47932 100644 --- a/src/batch/batch.service.ts +++ b/src/batch/batch.service.ts @@ -15,7 +15,7 @@ export class BatchService { private githubService: GithubService, private totalService: TotalService, ) {} - @Cron('0 0 * * * *') + @Cron('0 0 0 * * *') @Transactional({ isolationLevel: IsolationLevel.READ_COMMITTED, }) diff --git a/src/stat/dto/rank-list-option.dto.ts b/src/stat/dto/rank-list-option.dto.ts index 85e3d01..3cbd650 100644 --- a/src/stat/dto/rank-list-option.dto.ts +++ b/src/stat/dto/rank-list-option.dto.ts @@ -24,7 +24,7 @@ export class RankListDto { userId: string; @ApiProperty() - point: number; + score: number; @ApiProperty() nickname: string; diff --git a/src/stat/repository/github.repository.ts b/src/stat/repository/github.repository.ts index 94ada48..eca8cca 100644 --- a/src/stat/repository/github.repository.ts +++ b/src/stat/repository/github.repository.ts @@ -19,7 +19,7 @@ export class GithubRepository extends StatRepository { { githubId: github.githubId, accessToken: github.accessToken, - point: github.point, + point: github.score, }, ); } diff --git a/src/stat/repository/grade.repository.ts b/src/stat/repository/grade.repository.ts index 8f3c5b6..4a5095e 100644 --- a/src/stat/repository/grade.repository.ts +++ b/src/stat/repository/grade.repository.ts @@ -18,7 +18,7 @@ export class GradeRepository extends StatRepository { { userId: newGrade.userId }, { grade: newGrade.grade, - point: newGrade.point, + point: newGrade.score, }, ); } diff --git a/src/stat/service/algorithm.service.spec.ts b/src/stat/service/algorithm.service.spec.ts index 9d61521..9d96306 100644 --- a/src/stat/service/algorithm.service.spec.ts +++ b/src/stat/service/algorithm.service.spec.ts @@ -84,7 +84,7 @@ describe('AlgorithmService', () => { algorithm.rating = mockResponse.data.rating; algorithm.tier = mockResponse.data.tier; algorithm.solvedCount = mockResponse.data.solvedCount; - algorithm.point = 0; + algorithm.score = 91.96000000000001; algorithmRepository.save.mockResolvedValue(algorithm); await service.createAlgorithm(userId, bojId); @@ -133,7 +133,7 @@ describe('AlgorithmService', () => { algorithm.rating = 1500; algorithm.tier = 16; algorithm.solvedCount = 100; - algorithm.point = 0; + algorithm.score = 0; algorithmRepository.findOneById.mockResolvedValue(algorithm); const mockResponse = { @@ -175,7 +175,7 @@ describe('AlgorithmService', () => { algorithm.rating = 1500; algorithm.tier = 16; algorithm.solvedCount = 100; - algorithm.point = 0; + algorithm.score = 0; algorithmRepository.findOneById.mockResolvedValue(algorithm); const mockResponse = { diff --git a/src/stat/service/algorithm.service.ts b/src/stat/service/algorithm.service.ts index 14a7428..b1a30a5 100644 --- a/src/stat/service/algorithm.service.ts +++ b/src/stat/service/algorithm.service.ts @@ -8,9 +8,9 @@ import { import axios from 'axios'; import { AlgorithmRepository } from '../repository/algorithm.repository'; import { Algorithm } from '../../Entity/algorithm'; -import { NotFoundError } from 'rxjs'; import { RankListDto, RankListOptionDto } from '../dto/rank-list-option.dto'; import { PointFindDto } from '../dto/rank-find.dto'; +import { PERCENTILES, RATINGS } from '../../utils/algorithmData'; const URL = 'https://solved.ac/api/v3/user/show?handle='; @@ -49,12 +49,13 @@ export class AlgorithmService { algorithm.rating = bojInfo.rating; algorithm.tier = bojInfo.tier; algorithm.solvedCount = bojInfo.solvedCount; - algorithm.point = this.calculatePoint(bojInfo); + algorithm.score = this.calculatePoint(bojInfo); await this.algorithmRepository.save(algorithm); } async updateAlgorithm(userId: string) { - const algorithm = await this.algorithmRepository.findOneById(userId); + const algorithm: Algorithm = + await this.algorithmRepository.findOneById(userId); if (algorithm === null) { return; } @@ -63,7 +64,7 @@ export class AlgorithmService { algorithm.tier = bojInfo.tier; algorithm.rating = bojInfo.rating; algorithm.solvedCount = bojInfo.solvedCount; - algorithm.point = this.calculatePoint(bojInfo); + algorithm.score = this.calculatePoint(bojInfo); await this.algorithmRepository.updateAlgorithm(userId, algorithm); } catch (e) { if (e instanceof BadRequestException) { @@ -78,7 +79,8 @@ export class AlgorithmService { } async modifyAlgorithm(userId: string, bojId: string) { - const algorithm = await this.algorithmRepository.findOneById(userId); + const algorithm: Algorithm = + await this.algorithmRepository.findOneById(userId); if (algorithm === null) { throw new NotFoundException('Algorithm info not found'); } @@ -87,7 +89,7 @@ export class AlgorithmService { algorithm.tier = bojInfo.tier; algorithm.rating = bojInfo.rating; algorithm.solvedCount = bojInfo.solvedCount; - algorithm.point = this.calculatePoint(bojInfo); + algorithm.score = this.calculatePoint(bojInfo); await this.algorithmRepository.updateAlgorithm(userId, algorithm); } @@ -118,7 +120,27 @@ export class AlgorithmService { } private calculatePoint(bojInfo: BOJInfo) { - return 0; + const rating = bojInfo.rating; + if (rating <= RATINGS[0]) { + return 100 - PERCENTILES[0]; + } else if (rating >= RATINGS[RATINGS.length - 1]) { + return 100 - PERCENTILES[PERCENTILES.length - 1]; + } else { + for (let i = 1; i < RATINGS.length; i++) { + if (rating < RATINGS[i]) { + // 선형 보간법 + const x0 = RATINGS[i - 1], + x1 = RATINGS[i]; + const y0 = PERCENTILES[i - 1], + y1 = PERCENTILES[i]; + const percentile = + y0 + ((rating - x0) * (y1 - y0)) / (x1 - x0); + return 100 - percentile; + } + } + return 0; + } + // return bojInfo.rating; } public async getIndividualAlgorithmRank( diff --git a/src/stat/service/github.service.ts b/src/stat/service/github.service.ts index cb87087..0da8e52 100644 --- a/src/stat/service/github.service.ts +++ b/src/stat/service/github.service.ts @@ -10,6 +10,7 @@ import { Github } from '../../Entity/github'; import { CreateGithubDto } from '../dto/createGitub.dto'; import { RankListOptionDto } from '../dto/rank-list-option.dto'; import { PointFindDto } from '../dto/rank-find.dto'; +import { exponential_cdf, log_normal_cdf } from '../../utils/cdf'; @Injectable() export class GithubService { @@ -36,7 +37,7 @@ export class GithubService { const github = new Github(); github.userId = userId; - github.point = githubPoint; + github.score = githubPoint; github.accessToken = tokens.accessToken; github.githubId = userResource.id; await this.githubRepository.save(github); @@ -56,7 +57,7 @@ export class GithubService { const github = new Github(); github.userId = userId; - github.point = githubPoint; + github.score = githubPoint; github.accessToken = tokens.accessToken; github.githubId = userResource.id; await this.githubRepository.updateGithub(github); @@ -69,7 +70,7 @@ export class GithubService { } try { const githubInfo = await this.getUserResource(github.accessToken); - github.point = await this.calculateGithubPoint(githubInfo); + github.score = await this.calculateGithubPoint(githubInfo); await this.githubRepository.updateGithub(github); } catch (e) { this.logger.error( @@ -91,18 +92,23 @@ export class GithubService { } public async calculateGithubPoint(userResource: object) { const commitInfo = await this.getCommits(userResource['login']); - const PRInfo = await this.getPRs(userResource['login']); + const prInfo = await this.getPRs(userResource['login']); const issueInfo = await this.getIssues(userResource['login']); const followers = userResource['followers']; const [COMMIT_WEIGHT, PR_WEIGHT, ISSUE_WEIGHT, FOLLOWER_WEIGHT] = [ 2, 3, 2, 1, ]; - return ( - commitInfo * COMMIT_WEIGHT + - issueInfo * ISSUE_WEIGHT + - PRInfo * PR_WEIGHT + - followers * FOLLOWER_WEIGHT - ); + const TOTAL_WEIGHT = + COMMIT_WEIGHT + PR_WEIGHT + ISSUE_WEIGHT + FOLLOWER_WEIGHT; + + const point = + (COMMIT_WEIGHT * exponential_cdf(commitInfo / 250) + + ISSUE_WEIGHT * exponential_cdf(issueInfo / 25) + + PR_WEIGHT * exponential_cdf(prInfo / 50) + + FOLLOWER_WEIGHT * log_normal_cdf(followers / 10)) / + TOTAL_WEIGHT; + + return point * 100; } public async getIssues(userName: string) { diff --git a/src/stat/service/grade.service.ts b/src/stat/service/grade.service.ts index 0decf3f..62006b4 100644 --- a/src/stat/service/grade.service.ts +++ b/src/stat/service/grade.service.ts @@ -25,7 +25,7 @@ export class GradeService { const newGrade = new Grade(); newGrade.userId = userId; newGrade.grade = grade; - newGrade.point = this.calculatePoint(grade); + newGrade.score = this.calculatePoint(grade); await this.gradeRepository.save(newGrade); } @@ -40,7 +40,7 @@ export class GradeService { const newGrade = new Grade(); newGrade.userId = userId; newGrade.grade = grade; - newGrade.point = this.calculatePoint(grade); + newGrade.score = this.calculatePoint(grade); await this.gradeRepository.updateGrade(newGrade); } @@ -56,7 +56,9 @@ export class GradeService { } public calculatePoint(grade: number) { - return 0; + const maxGrade = 4.5; + const maxPercentage = 100; + return (grade / maxGrade) * maxPercentage; } public async getIndividualGradeRank(userId: string, options: PointFindDto) { diff --git a/src/stat/service/total.service.spec.ts b/src/stat/service/total.service.spec.ts index 8b16518..c8bb8ed 100644 --- a/src/stat/service/total.service.spec.ts +++ b/src/stat/service/total.service.spec.ts @@ -83,8 +83,8 @@ describe('TotalService', () => { const github = new Github(); const algorithm = new Algorithm(); const grade = new Grade(); - github.point = 123; - algorithm.point = 123; + github.score = 123; + algorithm.score = 123; grade.grade = 123; mockGitService.findGithub.mockResolvedValue(github); @@ -93,8 +93,8 @@ describe('TotalService', () => { const result = await service.findStat(userId); expect(result.grade).toEqual(grade.grade); - expect(result.githubPoint).toEqual(github.point); - expect(result.algorithmPoint).toEqual(algorithm.point); + expect(result.githubPoint).toEqual(github.score); + expect(result.algorithmPoint).toEqual(algorithm.score); }); it('should return null if stat does not exist', async function () { @@ -102,7 +102,7 @@ describe('TotalService', () => { const github = new Github(); const algorithm = null; const grade = new Grade(); - github.point = 123; + github.score = 123; grade.grade = 123; mockGitService.findGithub.mockResolvedValue(github); @@ -111,7 +111,7 @@ describe('TotalService', () => { const result = await service.findStat(userId); expect(result.grade).toEqual(grade.grade); - expect(result.githubPoint).toEqual(github.point); + expect(result.githubPoint).toEqual(github.score); expect(result.algorithmPoint).toEqual(null); }); }); diff --git a/src/stat/service/total.service.ts b/src/stat/service/total.service.ts index 34d85d5..4233aad 100644 --- a/src/stat/service/total.service.ts +++ b/src/stat/service/total.service.ts @@ -27,10 +27,10 @@ export class TotalService { const total = await this.totalRepository.findOneById(userId); return { - githubPoint: github ? github.point : null, - algorithmPoint: algorithm ? algorithm.point : null, + githubPoint: github ? github.score : null, + algorithmPoint: algorithm ? algorithm.score : null, grade: grade ? grade.grade : null, - totalPoint: grade ? total.point : null, + totalPoint: grade ? total.score : null, }; } @@ -52,7 +52,7 @@ export class TotalService { } const totalPoint = new TotalPoint(); totalPoint.userId = userId; - totalPoint.point = 0; + totalPoint.score = 0; await this.totalRepository.save(totalPoint); } @@ -62,12 +62,22 @@ export class TotalService { const algorithm = await this.algorithmService.findAlgorithm(userId); const grade = await this.gradeService.findGrade(userId); const total = new TotalPoint(); - total.point = this.calculateTotalPoint(github, algorithm, grade); + total.score = this.calculateTotalPoint(github, algorithm, grade); await this.totalRepository.updateTotal(total, userId); } calculateTotalPoint(github: Github, algorithm: Algorithm, grade: Grade) { - return 0; + const [GRADE_WEIGHT, ALGORITHM_WEIGHT, GITHUB_WEIGHT] = [1, 4, 4]; + const TOTAL_WEIGHT = GRADE_WEIGHT + ALGORITHM_WEIGHT + GITHUB_WEIGHT; + const githubPoint = github == null ? 0 : github.score; + const algorithmPoint = algorithm == null ? 0 : algorithm.score; + const gradePoint = grade == null ? 0 : grade.score; + const totalPoint = + (githubPoint * GITHUB_WEIGHT + + algorithmPoint * ALGORITHM_WEIGHT + + gradePoint * GRADE_WEIGHT) / + TOTAL_WEIGHT; + return totalPoint; } public async getIndividualTotalRank(userId: string, options: PointFindDto) { diff --git a/src/utils/algorithmData.ts b/src/utils/algorithmData.ts new file mode 100644 index 0000000..a0729af --- /dev/null +++ b/src/utils/algorithmData.ts @@ -0,0 +1,11 @@ +export const RATINGS: number[] = [ + 0, 30, 60, 90, 120, 150, 200, 300, 400, 500, 650, 800, 950, 1100, 1250, + 1400, 1600, 1750, 1900, 2000, 2100, 2200, 2300, 2400, 2500, 2600, 2700, + 2800, 2850, 2900, 2950, 3000, +]; + +export const PERCENTILES: number[] = [ + 100.0, 100.0, 92.73, 87.89, 82.22, 77.32, 70.19, 58.91, 50.9, 43.87, 36.36, + 30.36, 24.65, 19.65, 14.68, 10.32, 5.76, 3.32, 1.95, 1.45, 1.13, 0.86, 0.58, + 0.39, 0.29, 0.2, 0.13, 0.068, 0.044, 0.031, 0.024, 0.019, +]; diff --git a/src/utils/cdf.ts b/src/utils/cdf.ts new file mode 100644 index 0000000..d2c41dc --- /dev/null +++ b/src/utils/cdf.ts @@ -0,0 +1,8 @@ +export function exponential_cdf(x) { + return 1 - 2 ** -x; +} + +export function log_normal_cdf(x) { + // approximation + return x / (1 + x); +} diff --git a/src/utils/stat.repository.ts b/src/utils/stat.repository.ts index 935fa75..f5ddf64 100644 --- a/src/utils/stat.repository.ts +++ b/src/utils/stat.repository.ts @@ -15,21 +15,21 @@ export class StatRepository extends Repository { async findWithRank(options: RankListOptionDto): Promise<[RankListDto]> { const queryBuilder = this.createQueryBuilder() - .select(['b.rank', 'b.point', 'b.nickname']) + .select(['b.rank', 'b.score', 'b.nickname']) .addSelect('b.user_id', 'userId') .distinct(true) .from((sub) => { return sub - .select('RANK() OVER (ORDER BY a.point DESC)', 'rank') + .select('RANK() OVER (ORDER BY a.score DESC)', 'rank') .addSelect('a.user_id', 'user_id') - .addSelect('a.point', 'point') + .addSelect('a.score', 'score') .addSelect('u.nickname', 'nickname') .from(this.entity, 'a') .innerJoin(User, 'u', 'a.user_id = u.user_id') .where(this.createClassificationOption(options)); }, 'b') .where(this.createCursorOption(options)) - .orderBy('point', 'DESC') + .orderBy('score', 'DESC') .addOrderBy('userId') .limit(3); return await (>queryBuilder.getRawMany()); @@ -41,7 +41,7 @@ export class StatRepository extends Repository { .distinct(true) .from((sub) => { return sub - .select('RANK() OVER (ORDER BY a.point DESC)', 'rank') + .select('RANK() OVER (ORDER BY a.score DESC)', 'rank') .addSelect('a.user_id', 'user_id') .from(this.entity, 'a') .innerJoin(User, 'u', 'a.user_id = u.user_id') @@ -54,9 +54,9 @@ export class StatRepository extends Repository { } createCursorOption(options: RankListOptionDto) { if (!options.cursorPoint && !options.cursorUserId) { - return 'b.point > -1'; + return 'b.score > -1'; } else { - return `b.point < ${options.cursorPoint} or b.point = ${options.cursorPoint} AND b.user_id > '${options.cursorUserId}'`; + return `b.score < ${options.cursorPoint} or b.score = ${options.cursorPoint} AND b.user_id > '${options.cursorUserId}'`; } }